Skip to content

Commit

Permalink
Add generic TransformedShape and use it everywhere (#49)
Browse files Browse the repository at this point in the history
Previously, we had separate implementations of scale / translation and
screen-space transforms in both the 2D and 3D rendering code. This PR
adds a new `TransformedShape<S>` and uses it consistently.
  • Loading branch information
mkeeter authored Mar 25, 2024
1 parent 3b39c04 commit 60900ba
Show file tree
Hide file tree
Showing 13 changed files with 427 additions and 150 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
- Add `compare` operator (equivalent to `<=>` in C++ or `partial_cmp` in Rust,
with the difference that unordered results are returned as `NAN`)
- Fix a bug the x86 JIT evaluator's implementation of interval `abs`
- Add generic `TransformedShape<S>`, representing a shape transformed by a 4x4
homogeneous matrix
- This replaces `RenderConfig::mat` as the way to handle rotation / scale /
translation / perspective transforms, e.g. for interactive visualization
(where you don't want to remap the underlying shape)
- It's a more general solution: for example, we can use the same type to
change bounds for meshing (by translating + scaling the underlying model).

# 0.2.2
- Added many transcendental functions: `sin`, `cos`, `tan`, `asin`, `acos`,
Expand Down
5 changes: 1 addition & 4 deletions demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ fn run3d<S: fidget::eval::Shape>(
image_size: settings.size as usize,
tile_sizes: S::tile_sizes_3d().to_vec(),
threads: settings.threads,

mat,
};
let shape = shape.apply_transform(mat.into());

let mut depth = vec![];
let mut color = vec![];
Expand Down Expand Up @@ -207,8 +206,6 @@ fn run2d<S: fidget::eval::Shape>(
image_size: settings.size as usize,
tile_sizes: S::tile_sizes_2d().to_vec(),
threads: settings.threads,

mat: nalgebra::Transform2::identity(),
};
if sdf {
let mut image = vec![];
Expand Down
6 changes: 2 additions & 4 deletions fidget/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ arrayvec = "0.7"
bimap = "0.6.3"
document-features = "0.2"
ieee754 = "0.2"
nalgebra = "0.31"
num-derive = "0.3"
num-traits = "0.2"
ordered-float = "3"
static_assertions = "1"
thiserror = "1"
workspace-hack = { version = "0.1", path = "../workspace-hack" }

# JIT
dynasmrt = { version = "2.0", optional = true }
Expand All @@ -26,12 +28,8 @@ libc = { version = "0.2", optional = true }
# Rhai
rhai = { version = "1.17", optional = true, features = ["sync"] }

# Render
nalgebra = { version = "0.31" }

# Meshing
crossbeam-deque = { version = "0.8", optional = true }
workspace-hack = { version = "0.1", path = "../workspace-hack" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
Expand Down
4 changes: 0 additions & 4 deletions fidget/benches/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub fn prospero_size_sweep(c: &mut Criterion) {
image_size: size,
tile_sizes: fidget::vm::VmShape::tile_sizes_2d().to_vec(),
threads: 8,
mat: nalgebra::Transform2::identity(),
};
group.bench_function(BenchmarkId::new("vm", size), move |b| {
b.iter(|| {
Expand All @@ -39,7 +38,6 @@ pub fn prospero_size_sweep(c: &mut Criterion) {
image_size: size,
tile_sizes: fidget::jit::JitShape::tile_sizes_2d().to_vec(),
threads: 8,
mat: nalgebra::Transform2::identity(),
};
group.bench_function(BenchmarkId::new("jit", size), move |b| {
b.iter(|| {
Expand Down Expand Up @@ -69,7 +67,6 @@ pub fn prospero_thread_sweep(c: &mut Criterion) {
image_size: 1024,
tile_sizes: fidget::vm::VmShape::tile_sizes_2d().to_vec(),
threads,
mat: nalgebra::Transform2::identity(),
};
group.bench_function(BenchmarkId::new("vm", threads), move |b| {
b.iter(|| {
Expand All @@ -87,7 +84,6 @@ pub fn prospero_thread_sweep(c: &mut Criterion) {
image_size: 1024,
tile_sizes: fidget::jit::JitShape::tile_sizes_2d().to_vec(),
threads,
mat: nalgebra::Transform2::identity(),
};
group.bench_function(BenchmarkId::new("jit", threads), move |b| {
b.iter(|| {
Expand Down
15 changes: 15 additions & 0 deletions fidget/src/core/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod test;

mod bulk;
mod tracing;
mod transform;

pub mod types;

Expand All @@ -36,6 +37,7 @@ mod vars;
// Re-export a few things
pub use bulk::BulkEvaluator;
pub use tracing::TracingEvaluator;
pub use transform::TransformedShape;
pub use vars::Vars;

use types::{Grad, Interval};
Expand Down Expand Up @@ -170,6 +172,19 @@ pub trait Shape: Send + Sync + Clone {
fn simplify_tree_during_meshing(_d: usize) -> bool {
true
}

/// Associated type returned when applying a transform
///
/// This is normally [`TransformedShape<Self>`](TransformedShape), but if
/// `Self` is already `TransformedShape`, then the transform is stacked
/// (instead of creating a wrapped object).
type TransformedShape: Shape;

/// Returns a shape with the given transform applied
fn apply_transform(
self,
mat: nalgebra::Matrix4<f32>,
) -> <Self as Shape>::TransformedShape;
}

/// Extension trait for working with a shape without thinking much about memory
Expand Down
236 changes: 236 additions & 0 deletions fidget/src/core/eval/transform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use crate::{
eval::{BulkEvaluator, Interval, Shape, Tape, TracingEvaluator},
Error,
};
use nalgebra::{Matrix4, Point3, Vector3};

/// A generic [`Shape`] that has been transformed by a 4x4 transform matrix
#[derive(Clone)]
pub struct TransformedShape<S> {
shape: S,
mat: Matrix4<f32>,
}

impl<S> TransformedShape<S> {
/// Builds a new [`TransformedShape`] with the identity transform
pub fn new(shape: S, mat: Matrix4<f32>) -> Self {
Self { shape, mat }
}

/// Appends a translation to the transformation matrix
pub fn translate(&mut self, offset: Vector3<f32>) {
self.mat.append_translation_mut(&offset);
}

/// Appends a uniform scale to the transformation matrix
pub fn scale(&mut self, scale: f32) {
self.mat.append_scaling_mut(scale);
}

/// Resets to the identity transform matrix
pub fn reset(&mut self) {
self.mat = Matrix4::identity();
}

/// Sets the transform matrix
pub fn set_transform(&mut self, mat: Matrix4<f32>) {
self.mat = mat;
}
}

/// A generic [`Tape`] with an associated 4x4 transform matrix
pub struct TransformedTape<T> {
tape: T,
mat: Matrix4<f32>,
}

impl<T: Tape> Tape for TransformedTape<T> {
type Storage = <T as Tape>::Storage;
fn recycle(self) -> Self::Storage {
self.tape.recycle()
}
}

/// A generic [`TracingEvaluator`] which applies a transform matrix
#[derive(Default)]
pub struct TransformedTracingEval<E> {
eval: E,
}

trait Transformable {
fn transform(
x: Self,
y: Self,
z: Self,
mat: Matrix4<f32>,
) -> (Self, Self, Self)
where
Self: Sized;
}

impl Transformable for f32 {
fn transform(x: f32, y: f32, z: f32, mat: Matrix4<f32>) -> (f32, f32, f32) {
let out = mat.transform_point(&Point3::new(x, y, z));
(out.x, out.y, out.z)
}
}

impl Transformable for Interval {
fn transform(
x: Interval,
y: Interval,
z: Interval,
mat: Matrix4<f32>,
) -> (Interval, Interval, Interval) {
let out = [0, 1, 2, 3].map(|i| {
let row = mat.row(i);
x * row[0] + y * row[1] + z * row[2] + Interval::from(row[3])
});

(out[0] / out[3], out[1] / out[3], out[2] / out[3])
}
}

impl<T: TracingEvaluator> TracingEvaluator for TransformedTracingEval<T>
where
<T as TracingEvaluator>::Data: Transformable,
{
type Data = <T as TracingEvaluator>::Data;
type Tape = TransformedTape<<T as TracingEvaluator>::Tape>;
type TapeStorage = <T as TracingEvaluator>::TapeStorage;
type Trace = <T as TracingEvaluator>::Trace;
fn eval<F: Into<Self::Data>>(
&mut self,
tape: &Self::Tape,
x: F,
y: F,
z: F,
vars: &[f32],
) -> Result<(Self::Data, Option<&Self::Trace>), Error> {
let x = x.into();
let y = y.into();
let z = z.into();
let (x, y, z) = Transformable::transform(x, y, z, tape.mat);
self.eval.eval(&tape.tape, x, y, z, vars)
}
}

/// A generic [`BulkEvaluator`] which applies a transform matrix
#[derive(Default)]
pub struct TransformedBulkEval<E> {
eval: E,
xs: Vec<f32>,
ys: Vec<f32>,
zs: Vec<f32>,
}

impl<T: BulkEvaluator> BulkEvaluator for TransformedBulkEval<T> {
type Data = <T as BulkEvaluator>::Data;
type Tape = TransformedTape<<T as BulkEvaluator>::Tape>;
type TapeStorage = <T as BulkEvaluator>::TapeStorage;
fn eval(
&mut self,
tape: &Self::Tape,
x: &[f32],
y: &[f32],
z: &[f32],
vars: &[f32],
) -> Result<&[Self::Data], Error> {
if x.len() != y.len() || x.len() != z.len() {
return Err(Error::MismatchedSlices);
}
let n = x.len();
self.xs.resize(n, 0.0);
self.ys.resize(n, 0.0);
self.zs.resize(n, 0.0);
for i in 0..x.len() {
let p = tape.mat.transform_point(&Point3::new(x[i], y[i], z[i]));
self.xs[i] = p.x;
self.ys[i] = p.y;
self.zs[i] = p.z;
}
self.eval
.eval(&tape.tape, &self.xs, &self.ys, &self.zs, vars)
}
}

impl<S: Shape> Shape for TransformedShape<S> {
type Trace = <S as Shape>::Trace;
type Storage = <S as Shape>::Storage;
type Workspace = <S as Shape>::Workspace;
type TapeStorage = <S as Shape>::TapeStorage;
type PointEval = TransformedTracingEval<<S as Shape>::PointEval>;
type IntervalEval = TransformedTracingEval<<S as Shape>::IntervalEval>;
type FloatSliceEval = TransformedBulkEval<<S as Shape>::FloatSliceEval>;
type GradSliceEval = TransformedBulkEval<<S as Shape>::GradSliceEval>;
fn tile_sizes_2d() -> &'static [usize] {
S::tile_sizes_2d()
}
fn tile_sizes_3d() -> &'static [usize] {
S::tile_sizes_3d()
}
fn size(&self) -> usize {
self.shape.size()
}
fn recycle(self) -> Option<Self::Storage> {
self.shape.recycle()
}
fn point_tape(
&self,
storage: Self::TapeStorage,
) -> TransformedTape<<<S as Shape>::PointEval as TracingEvaluator>::Tape>
{
TransformedTape {
tape: self.shape.point_tape(storage),
mat: self.mat,
}
}
fn interval_tape(
&self,
storage: Self::TapeStorage,
) -> TransformedTape<<<S as Shape>::IntervalEval as TracingEvaluator>::Tape>
{
TransformedTape {
tape: self.shape.interval_tape(storage),
mat: self.mat,
}
}
fn float_slice_tape(
&self,
storage: Self::TapeStorage,
) -> TransformedTape<<<S as Shape>::FloatSliceEval as BulkEvaluator>::Tape>
{
TransformedTape {
tape: self.shape.float_slice_tape(storage),
mat: self.mat,
}
}
fn grad_slice_tape(
&self,
storage: Self::TapeStorage,
) -> TransformedTape<<<S as Shape>::GradSliceEval as BulkEvaluator>::Tape>
{
TransformedTape {
tape: self.shape.grad_slice_tape(storage),
mat: self.mat,
}
}
fn simplify(
&self,
trace: &Self::Trace,
storage: Self::Storage,
workspace: &mut Self::Workspace,
) -> Result<Self, Error> {
let shape = self.shape.simplify(trace, storage, workspace)?;
Ok(Self {
shape,
mat: self.mat,
})
}

type TransformedShape = Self;
fn apply_transform(mut self, mat: Matrix4<f32>) -> Self::TransformedShape {
self.mat *= mat;
self
}
}
13 changes: 13 additions & 0 deletions fidget/src/core/eval/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,19 @@ impl std::ops::Mul<Interval> for Interval {
}
}

impl std::ops::Mul<f32> for Interval {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
if self.has_nan() || rhs.is_nan() {
f32::NAN.into()
} else if rhs < 0.0 {
Interval::new(self.upper * rhs, self.lower * rhs)
} else {
Interval::new(self.lower * rhs, self.upper * rhs)
}
}
}

impl std::ops::Div<Interval> for Interval {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Expand Down
Loading

0 comments on commit 60900ba

Please sign in to comment.