From 60900ba02a6d6d9a28933a0fc3a466fd853f647b Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Mon, 25 Mar 2024 08:08:43 -0400 Subject: [PATCH] Add generic `TransformedShape` and use it everywhere (#49) 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` and uses it consistently. --- CHANGELOG.md | 7 + demo/src/main.rs | 5 +- fidget/Cargo.toml | 6 +- fidget/benches/render.rs | 4 - fidget/src/core/eval/mod.rs | 15 ++ fidget/src/core/eval/transform.rs | 236 ++++++++++++++++++++++++++++++ fidget/src/core/eval/types.rs | 13 ++ fidget/src/core/vm/mod.rs | 10 +- fidget/src/jit/mod.rs | 7 + fidget/src/render/config.rs | 78 ++++------ fidget/src/render/render2d.rs | 106 ++++++++++---- fidget/src/render/render3d.rs | 74 +++------- viewer/src/main.rs | 16 +- 13 files changed, 427 insertions(+), 150 deletions(-) create mode 100644 fidget/src/core/eval/transform.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 74eb2ec6..0b0b6b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`, 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`, diff --git a/demo/src/main.rs b/demo/src/main.rs index 7a8aa766..505e2a28 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -129,9 +129,8 @@ fn run3d( 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![]; @@ -207,8 +206,6 @@ fn run2d( 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![]; diff --git a/fidget/Cargo.toml b/fidget/Cargo.toml index 6e9b9c65..ea31b4fd 100644 --- a/fidget/Cargo.toml +++ b/fidget/Cargo.toml @@ -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 } @@ -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"] } diff --git a/fidget/benches/render.rs b/fidget/benches/render.rs index ff9d0b81..f173be20 100644 --- a/fidget/benches/render.rs +++ b/fidget/benches/render.rs @@ -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(|| { @@ -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(|| { @@ -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(|| { @@ -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(|| { diff --git a/fidget/src/core/eval/mod.rs b/fidget/src/core/eval/mod.rs index fea0a235..c1cfbbd4 100644 --- a/fidget/src/core/eval/mod.rs +++ b/fidget/src/core/eval/mod.rs @@ -28,6 +28,7 @@ pub mod test; mod bulk; mod tracing; +mod transform; pub mod types; @@ -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}; @@ -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`](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, + ) -> ::TransformedShape; } /// Extension trait for working with a shape without thinking much about memory diff --git a/fidget/src/core/eval/transform.rs b/fidget/src/core/eval/transform.rs new file mode 100644 index 00000000..cf9511d1 --- /dev/null +++ b/fidget/src/core/eval/transform.rs @@ -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 { + shape: S, + mat: Matrix4, +} + +impl TransformedShape { + /// Builds a new [`TransformedShape`] with the identity transform + pub fn new(shape: S, mat: Matrix4) -> Self { + Self { shape, mat } + } + + /// Appends a translation to the transformation matrix + pub fn translate(&mut self, offset: Vector3) { + 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) { + self.mat = mat; + } +} + +/// A generic [`Tape`] with an associated 4x4 transform matrix +pub struct TransformedTape { + tape: T, + mat: Matrix4, +} + +impl Tape for TransformedTape { + type Storage = ::Storage; + fn recycle(self) -> Self::Storage { + self.tape.recycle() + } +} + +/// A generic [`TracingEvaluator`] which applies a transform matrix +#[derive(Default)] +pub struct TransformedTracingEval { + eval: E, +} + +trait Transformable { + fn transform( + x: Self, + y: Self, + z: Self, + mat: Matrix4, + ) -> (Self, Self, Self) + where + Self: Sized; +} + +impl Transformable for f32 { + fn transform(x: f32, y: f32, z: f32, mat: Matrix4) -> (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, + ) -> (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 TracingEvaluator for TransformedTracingEval +where + ::Data: Transformable, +{ + type Data = ::Data; + type Tape = TransformedTape<::Tape>; + type TapeStorage = ::TapeStorage; + type Trace = ::Trace; + fn eval>( + &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 { + eval: E, + xs: Vec, + ys: Vec, + zs: Vec, +} + +impl BulkEvaluator for TransformedBulkEval { + type Data = ::Data; + type Tape = TransformedTape<::Tape>; + type TapeStorage = ::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 Shape for TransformedShape { + type Trace = ::Trace; + type Storage = ::Storage; + type Workspace = ::Workspace; + type TapeStorage = ::TapeStorage; + type PointEval = TransformedTracingEval<::PointEval>; + type IntervalEval = TransformedTracingEval<::IntervalEval>; + type FloatSliceEval = TransformedBulkEval<::FloatSliceEval>; + type GradSliceEval = TransformedBulkEval<::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.shape.recycle() + } + fn point_tape( + &self, + storage: Self::TapeStorage, + ) -> TransformedTape<<::PointEval as TracingEvaluator>::Tape> + { + TransformedTape { + tape: self.shape.point_tape(storage), + mat: self.mat, + } + } + fn interval_tape( + &self, + storage: Self::TapeStorage, + ) -> TransformedTape<<::IntervalEval as TracingEvaluator>::Tape> + { + TransformedTape { + tape: self.shape.interval_tape(storage), + mat: self.mat, + } + } + fn float_slice_tape( + &self, + storage: Self::TapeStorage, + ) -> TransformedTape<<::FloatSliceEval as BulkEvaluator>::Tape> + { + TransformedTape { + tape: self.shape.float_slice_tape(storage), + mat: self.mat, + } + } + fn grad_slice_tape( + &self, + storage: Self::TapeStorage, + ) -> TransformedTape<<::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 { + let shape = self.shape.simplify(trace, storage, workspace)?; + Ok(Self { + shape, + mat: self.mat, + }) + } + + type TransformedShape = Self; + fn apply_transform(mut self, mat: Matrix4) -> Self::TransformedShape { + self.mat *= mat; + self + } +} diff --git a/fidget/src/core/eval/types.rs b/fidget/src/core/eval/types.rs index a50a0c13..2d51e0f8 100644 --- a/fidget/src/core/eval/types.rs +++ b/fidget/src/core/eval/types.rs @@ -621,6 +621,19 @@ impl std::ops::Mul for Interval { } } +impl std::ops::Mul 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 for Interval { type Output = Self; fn div(self, rhs: Self) -> Self { diff --git a/fidget/src/core/vm/mod.rs b/fidget/src/core/vm/mod.rs index ba0f998c..ad0e8d60 100644 --- a/fidget/src/core/vm/mod.rs +++ b/fidget/src/core/vm/mod.rs @@ -5,10 +5,11 @@ use crate::{ eval::{ types::{Grad, Interval}, BulkEvaluator, MathShape, Shape, ShapeVars, Tape, Trace, - TracingEvaluator, + TracingEvaluator, TransformedShape, }, Context, Error, }; +use nalgebra::Matrix4; use std::{collections::HashMap, sync::Arc}; mod choice; @@ -176,6 +177,11 @@ impl Shape for GenericVmShape { fn tile_sizes_2d() -> &'static [usize] { &[256, 128, 64, 32, 16, 8] } + + type TransformedShape = TransformedShape; + fn apply_transform(self, mat: Matrix4) -> Self::TransformedShape { + TransformedShape::new(self, mat) + } } impl MathShape for GenericVmShape { @@ -328,7 +334,7 @@ impl TracingEvaluator for VmIntervalEval { v[out] = v[arg] + imm.into(); } RegOp::MulRegImm(out, arg, imm) => { - v[out] = v[arg] * imm.into(); + v[out] = v[arg] * imm; } RegOp::DivRegImm(out, arg, imm) => { v[out] = v[arg] / imm.into(); diff --git a/fidget/src/jit/mod.rs b/fidget/src/jit/mod.rs index 2978628e..e3addff4 100644 --- a/fidget/src/jit/mod.rs +++ b/fidget/src/jit/mod.rs @@ -28,6 +28,7 @@ use crate::{ eval::{ types::{Grad, Interval}, BulkEvaluator, MathShape, Shape, ShapeVars, Tape, TracingEvaluator, + TransformedShape, }, jit::mmap::Mmap, vm::{Choice, GenericVmShape, VmData, VmTrace, VmWorkspace}, @@ -37,6 +38,7 @@ use dynasmrt::{ components::PatchLoc, dynasm, AssemblyOffset, DynamicLabel, DynasmApi, DynasmError, DynasmLabelApi, TargetKind, }; +use nalgebra::Matrix4; use std::collections::HashMap; mod mmap; @@ -843,6 +845,11 @@ impl Shape for JitShape { fn size(&self) -> usize { self.0.size() } + + type TransformedShape = TransformedShape; + fn apply_transform(self, mat: Matrix4) -> Self::TransformedShape { + TransformedShape::new(self, mat) + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/fidget/src/render/config.rs b/fidget/src/render/config.rs index ee62b591..433ee47c 100644 --- a/fidget/src/render/config.rs +++ b/fidget/src/render/config.rs @@ -1,17 +1,12 @@ use crate::{eval::Shape, render::RenderMode, Error}; use nalgebra::{ - allocator::Allocator, geometry::Transform, Const, DefaultAllocator, - DimNameAdd, DimNameSub, DimNameSum, U1, + allocator::Allocator, Const, DefaultAllocator, DimNameAdd, DimNameSub, + DimNameSum, U1, }; use std::sync::atomic::{AtomicUsize, Ordering}; /// Container to store render configuration (resolution, etc) -pub struct RenderConfig -where - nalgebra::Const: nalgebra::DimNameAdd, - DefaultAllocator: - Allocator, U1>, DimNameSum, U1>>, -{ +pub struct RenderConfig { /// Image size (for a square output image) pub image_size: usize, @@ -23,20 +18,9 @@ where /// Number of threads to use; 8 by default pub threads: usize, - - /// Transform matrix to apply to the input coordinates - /// - /// By default, we render a cube spanning ±1 on all axes; `mat` allows for - /// rotation, scaling, transformation, and even perspective. - pub mat: Transform, } -impl Default for RenderConfig -where - nalgebra::Const: nalgebra::DimNameAdd, - DefaultAllocator: - Allocator, U1>, DimNameSum, U1>>, -{ +impl Default for RenderConfig { fn default() -> Self { Self { image_size: 512, @@ -45,7 +29,6 @@ where _ => vec![128, 64, 32, 16, 8], }, threads: 8, - mat: Transform::identity(), } } } @@ -65,9 +48,9 @@ where as DimNameAdd>>::Output: DimNameSub>, { - /// Returns a modified `RenderConfig` where `mat` is adjusted based on image - /// size, and the image size is padded to an even multiple of `tile_size`. - pub(crate) fn align(&self) -> AlignedRenderConfig { + /// Returns a `RenderConfig` where the image size is padded to an even + /// multiple of `tile_size`, and `mat` is populated based on image size. + pub(crate) fn align(&self) -> (AlignedRenderConfig, NPlusOneMatrix) { let mut tile_sizes: Vec = self .tile_sizes .iter() @@ -98,20 +81,21 @@ where U1, >>::Buffer, >::from_element(-1.0); - let mat = self.mat.matrix() - * nalgebra::Transform::::identity() - .matrix() - .append_scaling(2.0 / image_size as f32) - .append_scaling(scale) - .append_translation(&v); + let mat = nalgebra::Transform::::identity() + .matrix() + .append_scaling(2.0 / image_size as f32) + .append_scaling(scale) + .append_translation(&v); - AlignedRenderConfig { - image_size, - orig_image_size: self.image_size, - tile_sizes, - threads: self.threads, + ( + AlignedRenderConfig { + image_size, + orig_image_size: self.image_size, + tile_sizes, + threads: self.threads, + }, mat, - } + ) } } @@ -129,8 +113,6 @@ where pub tile_sizes: Vec, pub threads: usize, - - pub mat: NPlusOneMatrix, } /// Type for a static `f32` matrix of size `N + 1` @@ -236,22 +218,21 @@ mod test { image_size: 512, tile_sizes: vec![64, 32], threads: 8, - mat: Transform::identity(), }; - let aligned = config.align(); + let (aligned, mat) = config.align(); assert_eq!(aligned.image_size, config.image_size); assert_eq!(aligned.tile_sizes, config.tile_sizes); assert_eq!(aligned.threads, config.threads); assert_eq!( - aligned.mat.transform_point(&Point2::new(0.0, 0.0)), + mat.transform_point(&Point2::new(0.0, 0.0)), Point2::new(-1.0, -1.0) ); assert_eq!( - aligned.mat.transform_point(&Point2::new(512.0, 0.0)), + mat.transform_point(&Point2::new(512.0, 0.0)), Point2::new(1.0, -1.0) ); assert_eq!( - aligned.mat.transform_point(&Point2::new(512.0, 512.0)), + mat.transform_point(&Point2::new(512.0, 512.0)), Point2::new(1.0, 1.0) ); @@ -259,25 +240,22 @@ mod test { image_size: 575, tile_sizes: vec![64, 32], threads: 8, - mat: Transform::identity(), }; - let aligned = config.align(); + let (aligned, mat) = config.align(); assert_eq!(aligned.orig_image_size, 575); assert_eq!(aligned.image_size, 576); assert_eq!(aligned.tile_sizes, config.tile_sizes); assert_eq!(aligned.threads, config.threads); assert_eq!( - aligned.mat.transform_point(&Point2::new(0.0, 0.0)), + mat.transform_point(&Point2::new(0.0, 0.0)), Point2::new(-1.0, -1.0) ); assert_eq!( - aligned - .mat - .transform_point(&Point2::new(config.image_size as f32, 0.0)), + mat.transform_point(&Point2::new(config.image_size as f32, 0.0)), Point2::new(1.0, -1.0) ); assert_eq!( - aligned.mat.transform_point(&Point2::new( + mat.transform_point(&Point2::new( config.image_size as f32, config.image_size as f32 )), diff --git a/fidget/src/render/render2d.rs b/fidget/src/render/render2d.rs index 1ff61fec..9a5575af 100644 --- a/fidget/src/render/render2d.rs +++ b/fidget/src/render/render2d.rs @@ -4,7 +4,7 @@ use crate::{ eval::{types::Interval, BulkEvaluator, Shape, TracingEvaluator}, render::config::{AlignedRenderConfig, Queue, RenderConfig, Tile}, }; -use nalgebra::{Point2, Vector2}; +use nalgebra::Point2; use std::sync::Arc; //////////////////////////////////////////////////////////////////////////////// @@ -199,25 +199,9 @@ impl Worker<'_, S, M> { let tile_size = self.config.tile_sizes[depth]; // Brute-force way to find the (interval) bounding box of the region - let mut x_min = f32::INFINITY; - let mut x_max = f32::NEG_INFINITY; - let mut y_min = f32::INFINITY; - let mut y_max = f32::NEG_INFINITY; - let base = Point2::from(tile.corner); - for i in 0..4 { - let offset = Vector2::new( - if (i & 1) == 0 { 0 } else { tile_size }, - if (i & 2) == 0 { 0 } else { tile_size }, - ); - let p = (base + offset).cast::(); - let p = self.config.mat.transform_point(&p); - x_min = x_min.min(p.x); - x_max = x_max.max(p.x); - y_min = y_min.min(p.y); - y_max = y_max.max(p.y); - } - let x = Interval::new(x_min, x_max); - let y = Interval::new(y_min, y_max); + let base = Point2::from(tile.corner).cast::(); + 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(0.0, 0.0); let (i, simplify) = self @@ -276,12 +260,8 @@ impl Worker<'_, S, M> { let mut index = 0; for j in 0..tile_size { for i in 0..tile_size { - let p = self.config.mat.transform_point(&Point2::new( - (tile.corner[0] + i) as f32, - (tile.corner[1] + j) as f32, - )); - self.scratch.x[index] = p.x; - self.scratch.y[index] = p.y; + self.scratch.x[index] = (tile.corner[0] + i) as f32; + self.scratch.y[index] = (tile.corner[1] + j) as f32; index += 1; } } @@ -354,12 +334,25 @@ pub fn render( config: &RenderConfig<2>, mode: &M, ) -> Vec { - let config = config.align(); + let (config, mat) = config.align(); assert!(config.image_size % config.tile_sizes[0] == 0); for i in 0..config.tile_sizes.len() - 1 { assert!(config.tile_sizes[i] % config.tile_sizes[i + 1] == 0); } + // Convert to a 4x4 matrix and apply to the shape + let mat = mat.insert_row(2, 0.0); + let mat = mat.insert_column(2, 0.0); + let shape = shape.apply_transform(mat); + + render_inner(shape, config, mode) +} + +fn render_inner( + shape: S, + config: AlignedRenderConfig<2>, + mode: &M, +) -> Vec { let mut tiles = vec![]; for i in 0..config.image_size / config.tile_sizes[0] { for j in 0..config.image_size / config.tile_sizes[0] { @@ -485,6 +478,49 @@ mod test { render_and_compare(shape, EXPECTED); } + fn check_hi_transformed() { + let (ctx, root) = Context::from_text(HI.as_bytes()).unwrap(); + let shape = S::new(&ctx, root).unwrap(); + let mut mat = nalgebra::Matrix4::::identity(); + mat.prepend_translation_mut(&nalgebra::Vector3::new(0.5, 0.5, 0.0)); + mat.prepend_scaling_mut(0.5); + let shape = shape.apply_transform(mat); + const EXPECTED: &str = " + .XXX............................ + .XXX............................ + .XXX............................ + .XXX............................ + .XXX............................ + .XXX............................ + .XXX............................ + .XXX....................XXX..... + .XXX...................XXXXX.... + .XXX...................XXXXX.... + .XXX...................XXXX..... + .XXX............................ + .XXX............................ + .XXX............................ + .XXX..XXXXXX............XXX..... + .XXXXXXXXXXXXX..........XXX..... + .XXXXXXXXXXXXXXX........XXX..... + .XXXXXX....XXXXX........XXX..... + .XXXXX.......XXXX.......XXX..... + .XXXX.........XXX.......XXX..... + .XXX..........XXXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + .XXX...........XXX......XXX..... + ................................"; + render_and_compare(shape, EXPECTED); + } + fn check_quarter() { let (ctx, root) = Context::from_text(QUARTER.as_bytes()).unwrap(); let shape = S::new(&ctx, root).unwrap(); @@ -540,6 +576,22 @@ mod test { check_hi::(); } + #[test] + fn render_hi_transformed_vm() { + check_hi_transformed::(); + } + + #[test] + fn render_hi_transformed_vm3() { + check_hi_transformed::>(); + } + + #[cfg(feature = "jit")] + #[test] + fn render_hi_transformed_jit() { + check_hi_transformed::(); + } + #[test] fn render_quarter_vm() { check_quarter::(); diff --git a/fidget/src/render/render3d.rs b/fidget/src/render/render3d.rs index 0a118c48..a0847e2a 100644 --- a/fidget/src/render/render3d.rs +++ b/fidget/src/render/render3d.rs @@ -5,7 +5,7 @@ use crate::{ render::config::{AlignedRenderConfig, Queue, RenderConfig, Tile}, }; -use nalgebra::{Point3, Vector3}; +use nalgebra::Point3; use std::{collections::HashMap, sync::Arc}; //////////////////////////////////////////////////////////////////////////////// @@ -71,33 +71,10 @@ impl Worker<'_, S> { return; } - // Brute-force way to find the (interval) bounding box of the region - let mut x_min = f32::INFINITY; - let mut x_max = f32::NEG_INFINITY; - let mut y_min = f32::INFINITY; - let mut y_max = f32::NEG_INFINITY; - let mut z_min = f32::INFINITY; - let mut z_max = f32::NEG_INFINITY; - let base = Point3::from(tile.corner); - for i in 0..8 { - let offset = Vector3::new( - if (i & 1) == 0 { 0 } else { tile_size }, - if (i & 2) == 0 { 0 } else { tile_size }, - if (i & 4) == 0 { 0 } else { tile_size }, - ); - let p = (base + offset).cast::(); - let p = self.config.mat.transform_point(&p); - x_min = x_min.min(p.x); - x_max = x_max.max(p.x); - y_min = y_min.min(p.y); - y_max = y_max.max(p.y); - z_min = z_min.min(p.z); - z_max = z_max.max(p.z); - } - - let x = Interval::new(x_min, x_max); - let y = Interval::new(y_min, y_max); - let z = Interval::new(z_min, z_max); + let base = Point3::from(tile.corner).cast::(); + 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 @@ -178,18 +155,7 @@ impl Worker<'_, S> { continue; } - // The matrix transformation is separable until the final - // division by w. We can precompute the XY-1 portion of the - // multiplication here, since it's shared by every voxel in this - // column of the image. - let v = ((tile.corner[0] + i) as f32) * self.config.mat.column(0) - + ((tile.corner[1] + j) as f32) * self.config.mat.column(1) - + self.config.mat.column(3); - for k in (0..tile_size).rev() { - let v = v - + ((tile.corner[2] + k) as f32) * self.config.mat.column(2); - // SAFETY: // Index cannot exceed tile_size**3, which is (a) the size // that we allocated in `Scratch::new` and (b) checked by @@ -198,9 +164,12 @@ impl Worker<'_, S> { // Using unsafe indexing here is a roughly 2.5% speedup, // since this is the hottest loop. unsafe { - *self.scratch.x.get_unchecked_mut(index) = v.x / v.w; - *self.scratch.y.get_unchecked_mut(index) = v.y / v.w; - *self.scratch.z.get_unchecked_mut(index) = v.z / v.w; + *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; } @@ -253,14 +222,9 @@ impl Worker<'_, S> { // We step one voxel above the surface to reduce // glitchiness on edges and corners, where rendering // inside the surface could pick the wrong normal. - let p = self.config.mat.transform_point(&Point3::new( - (tile.corner[0] + i) as f32, - (tile.corner[1] + j) as f32, - (tile.corner[2] + k + 1) as f32, - )); - self.scratch.x[grad] = p.x; - self.scratch.y[grad] = p.y; - self.scratch.z[grad] = p.z; + self.scratch.x[grad] = (tile.corner[0] + i) as f32; + self.scratch.y[grad] = (tile.corner[1] + j) as f32; + self.scratch.z[grad] = (tile.corner[2] + k) as f32; // This can only be called once per iteration, so we'll // never overwrite parts of columns that are still used @@ -381,12 +345,20 @@ pub fn render( shape: S, config: &RenderConfig<3>, ) -> (Vec, Vec<[u8; 3]>) { - let config = config.align(); + let (config, mat) = config.align(); assert!(config.image_size % config.tile_sizes[0] == 0); for i in 0..config.tile_sizes.len() - 1 { assert!(config.tile_sizes[i] % config.tile_sizes[i + 1] == 0); } + let shape = shape.apply_transform(mat); + render_inner(shape, config) +} + +pub fn render_inner( + shape: S, + config: AlignedRenderConfig<3>, +) -> (Vec, Vec<[u8; 3]>) { let mut tiles = vec![]; for i in 0..config.image_size / config.tile_sizes[0] { for j in 0..config.image_size / config.tile_sizes[0] { diff --git a/viewer/src/main.rs b/viewer/src/main.rs index 383aa583..96b47934 100644 --- a/viewer/src/main.rs +++ b/viewer/src/main.rs @@ -5,7 +5,7 @@ use eframe::egui; use env_logger::Env; use fidget::render::RenderConfig; use log::{debug, error, info}; -use nalgebra::{Transform2, Transform3, Vector2, Vector3}; +use nalgebra::{Transform3, Vector3}; use notify::Watcher; use std::{error::Error, path::Path}; @@ -149,13 +149,14 @@ fn render( ) { match mode { RenderMode::TwoD(camera, mode) => { - let mat = Transform2::from_matrix_unchecked( - Transform2::identity() + let mat = Transform3::from_matrix_unchecked( + Transform3::identity() .matrix() .append_scaling(camera.scale) - .append_translation(&Vector2::new( + .append_translation(&Vector3::new( camera.offset.x, camera.offset.y, + 0.0, )), ); @@ -163,9 +164,9 @@ fn render( image_size, tile_sizes: S::tile_sizes_2d().to_vec(), threads: 8, - - mat, }; + let shape = shape.apply_transform(mat.into()); + match mode { TwoDMode::Color => { let image = fidget::render::render2d( @@ -226,9 +227,8 @@ fn render( image_size, tile_sizes: S::tile_sizes_2d().to_vec(), threads: 8, - - mat, }; + let shape = shape.apply_transform(mat.into()); let (depth, color) = fidget::render::render3d(shape, &config); match mode { ThreeDMode::Color => {