From a6b3d47c629899e87f540a1ec47d45214c40cde7 Mon Sep 17 00:00:00 2001 From: FredericaBernkastel Date: Fri, 24 Sep 2021 00:04:40 +0600 Subject: [PATCH] Shape::{union, subtraction, intersection, smooth_min}; Added more shapes --- Cargo.toml | 2 +- src/argmax2d.rs | 6 +- src/drawing/impl_draw_rgbaimage.rs | 6 +- src/geometry/mod.rs | 33 +++- src/geometry/shapes.rs | 308 +++++++++++++++++++++++++++-- src/lib.rs | 2 +- src/sdf.rs | 105 +++++++++- 7 files changed, 430 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17e2a40..a234ecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "space-filling" -version = "0.3.0" +version = "0.3.1" description = "Generalized 2D space filling" readme = "readme.md" authors = ["Frederica Bernkastel "] diff --git a/src/argmax2d.rs b/src/argmax2d.rs index f0e5e9a..202b1e6 100644 --- a/src/argmax2d.rs +++ b/src/argmax2d.rs @@ -96,8 +96,8 @@ fn offset_to_xy(offset: u64, width: u64) -> Point2D { ].into() } -fn xy_to_offset(xy: Point2D, width: u64) -> u64 { - xy.y as u64 * width + xy.x as u64 +fn xy_to_offset(xy: Point2D, width: u64) -> u64 { + xy.y * width + xy.x } impl Argmax2D { @@ -168,7 +168,7 @@ impl Argmax2D { ) * self.resolution as f32; let chunk_span = (domain / self.chunk_size as f32) .round_out() - .to_i64(); + .cast::(); (chunk_span.min.y .. chunk_span.max.y) .into_par_iter() diff --git a/src/drawing/impl_draw_rgbaimage.rs b/src/drawing/impl_draw_rgbaimage.rs index a3be3bc..363dbee 100644 --- a/src/drawing/impl_draw_rgbaimage.rs +++ b/src/drawing/impl_draw_rgbaimage.rs @@ -125,14 +125,14 @@ fn rescale_texture(texture: &DynamicImage, size: Size2D) -> Dyn ).resize_exact(size.width, size.height, FilterType::Triangle) } -fn sdf_overlay_aa(sdf: f32, Δp: f32, col1: Rgba, mut col2: Rgba) -> Rgba { +fn sdf_overlay_aa(sdf: f32, Δp: f32, mut col1: Rgba, mut col2: Rgba) -> Rgba { let Δf = (0.5 * Δp - sdf) // antialias .clamp(0.0, Δp); let alpha = Δf / Δp; // overlay blending with premultiplied alpha col2.0[3] = ((col2.0[3] as f32) * alpha) as u8; - col2.blend(&col1); - col2 + col1.blend(&col2); + col1 } /// Draw shapes, parallel. diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs index e314344..69b832b 100644 --- a/src/geometry/mod.rs +++ b/src/geometry/mod.rs @@ -1,22 +1,22 @@ //! . //! -//! The origin of coordinate system is in top-left corner. All shapes are represented in the -//! interval [-1, 1], and center in the origin. +//! The origin of coordinate system is in top-left corner. Most of shapes are represented in the +//! interval `[-1, 1]`, and center in the origin. use { std::ops::Mul, euclid::{Point2D, Box2D, Vector2D as V2, Size2D, Rotation2D, Angle}, num_traits::NumCast, - crate::sdf::SDF + crate::sdf::{SDF, Union, Subtraction, Intersection, SmoothMin} }; pub mod shapes; pub use shapes::*; -/// Pixel coordinate system +/// Pixel coordinate basis #[derive(Debug, Copy, Clone)] pub struct PixelSpace; -/// Normalized coordinate system +/// Normalized coordinate basis #[derive(Debug, Copy, Clone)] pub struct WorldSpace; @@ -29,12 +29,33 @@ pub trait Shape: SDF + BoundingBox { fn translate(self, offset: V2) -> Translation where Self: Sized { Translation { shape: self, offset } } + /// Rotate around the center of shape's bounding box fn rotate(self, angle: Angle) -> Rotation where Self: Sized { Rotation { shape: self, angle } } + /// Scale around the center of shape's bounding box fn scale(self, scale: V2) -> Scale where Self: Sized { Scale { shape: self, scale } } + /// Union of two SDFs. + fn union(self, other: U) -> Union where Self: Sized { + Union { s1: self, s2: other } + } + /// Subtracion of two SDFs. Note that this operation is *not* commutative, + /// i.e. `Subtraction {a, b} =/= Subtraction {b, a}`. + fn subtraction(self, other: U) -> Subtraction where Self: Sized { + Subtraction { s1: self, s2: other } + } + /// Intersection of two SDFs. + fn intersection(self, other: U) -> Intersection where Self: Sized { + Intersection { s1: self, s2: other } + } + /// Takes the minimum of two SDFs, smoothing between them when they are close. + /// + /// `k` controls the radius/distance of the smoothing. 32 is a good default value. + fn smooth_min(self, other: U, k: T) -> SmoothMin where Self: Sized { + SmoothMin { s1: self, s2: other, k } + } #[cfg(feature = "drawing")] #[cfg_attr(doc, doc(cfg(feature = "drawing")))] fn texture(self, texture: T) -> crate::drawing::Texture where Self: Sized { @@ -55,6 +76,7 @@ impl BoundingBox for Translation } } +/// Rotate around the center of shape's bounding box #[derive(Debug, Copy, Clone)] pub struct Rotation { pub shape: S, @@ -72,6 +94,7 @@ impl BoundingBox for Rotation } } +/// Scale around the center of shape's bounding box #[derive(Debug, Copy, Clone)] pub struct Scale { pub shape: S, diff --git a/src/geometry/shapes.rs b/src/geometry/shapes.rs index 36e5295..fac6092 100644 --- a/src/geometry/shapes.rs +++ b/src/geometry/shapes.rs @@ -1,16 +1,15 @@ +#![allow(non_upper_case_globals)] use { - super::{BoundingBox, WorldSpace}, - crate::sdf::SDF, - euclid::{Box2D, Point2D, Vector2D as V2} + super::{BoundingBox, WorldSpace, Translation, Shape}, + crate::sdf::{SDF, Union}, + euclid::{Box2D, Point2D, Vector2D as V2}, + std::marker::PhantomData }; /// Unit circle #[derive(Debug, Copy, Clone)] pub struct Circle; -#[derive(Debug, Copy, Clone)] -pub struct Square; - impl BoundingBox for Circle { fn bounding_box(&self) -> Box2D { Box2D::new( @@ -18,24 +17,28 @@ impl BoundingBox for Circle { Point2D::splat(1.0) )}} -impl BoundingBox for Square { - fn bounding_box(&self) -> Box2D { - Box2D::new( - Point2D::splat(-1.0), - Point2D::splat(1.0) - )}} - impl SDF for Circle { fn sdf(&self, pixel: Point2D) -> f32 { pixel.to_vector().length() - 1.0 } } -impl SDF for Square { +/// Rectangle with center at `[0, 0]` +#[derive(Debug, Copy, Clone)] +pub struct Rect { + pub size: Point2D +} + +impl BoundingBox for Rect { + fn bounding_box(&self) -> Box2D { + Box2D::new( + -self.size / 2.0, + self.size / 2.0 + )}} + +impl SDF for Rect { fn sdf(&self, pixel: Point2D) -> f32 { - //let pixel = self.center().to_vector() - pixel.to_vector(); - //let dist = pixel.to_vector().abs() - (self.size.to_vector() / 2.0); - let dist = pixel.to_vector().abs() - V2::splat(1.0); + let dist = pixel.to_vector().abs() - (self.size.to_vector() / 2.0); let outside_dist = dist .max(V2::splat(0.0)) .length(); @@ -43,5 +46,276 @@ impl SDF for Square { .max(dist.y) .min(0.0); outside_dist + inside_dist + }} + +#[derive(Debug, Copy, Clone)] +pub struct Line { + pub a: Point2D, + pub b: Point2D, + pub thickness: T, +} + +impl BoundingBox for Line { + fn bounding_box(&self) -> Box2D { + let ret = Box2D::from_points([self.a, self.b]); + let t = V2::splat(self.thickness / 2.0); + Box2D::new(ret.min - t, ret.max + t) + }} + +impl SDF for Line { + fn sdf(&self, pixel: Point2D) -> f32 { + let ba = self.b - self.a; + let pa = pixel - self.a; + let h = (pa.dot(ba) / ba.dot(ba)).clamp(0.0, 1.0); + (pa - ba * h).length() - self.thickness / 2.0 + } +} + +/// Regular polygon with N sides, inscribed in a unit circle. Partially evaluated at compile-time. +#[derive(Debug, Copy, Clone)] +pub struct NGonC; + +impl BoundingBox for NGonC { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::splat(-1.0), + Point2D::splat(1.0) + )}} + +impl SDF for NGonC { + fn sdf(&self, pixel: Point2D) -> f32 { + use std::f32::consts::*; + let angle = pixel.y.atan2(pixel.x) + FRAC_PI_2; + let split = TAU / N as f32; + let r = (PI / N as f32).cos(); + pixel.to_vector().length() * (split * (angle / split + 0.5).floor() - angle).cos() - r + } +} + +/// Regular polygon with N sides, inscribed in a unit circle. Evaluated at runtime. +#[derive(Debug, Copy, Clone)] +pub struct NGonR { + pub n: u64 +} + +impl BoundingBox for NGonR { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::splat(-1.0), + Point2D::splat(1.0) + )}} + +impl SDF for NGonR { + fn sdf(&self, pixel: Point2D) -> f32 { + use std::f32::consts::*; + let angle = pixel.y.atan2(pixel.x) + FRAC_PI_2; + let split = TAU / self.n as f32; + let r = (PI / self.n as f32).cos(); + pixel.to_vector().length() * (split * (angle / split + 0.5).floor() - angle).cos() - r + } +} + +/// N-pointed regular star polygon, inscibed in a unit circle. +/// `m` is density, must be between `2..=n` +#[derive(Debug, Copy, Clone)] +pub struct Star { + pub n: u64, + pub m: f32 +} + +impl BoundingBox for Star { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::splat(-1.0), + Point2D::splat(1.0) + )}} + +impl SDF for Star { + fn sdf(&self, pixel: Point2D) -> f32 { + use std::f32::consts::*; + let module = |x: f32, y: f32| x - y * (x / y).floor(); + let an = PI / self.n as f32; + let en = PI / self.m; + let acs = V2::<_, WorldSpace>::new(an.cos(), an.sin()); + let ecs = V2::new(en.cos(), en.sin()); + + let bn = module(pixel.x.atan2(pixel.y), 2.0 * an) - an; + let mut p = V2::new(bn.cos(), bn.sin().abs()) + * pixel.to_vector().length() + - acs; + p += ecs * (-p.dot(ecs)).clamp(0.0, acs.y / ecs.y); + p.length() * p.x.signum() + } +} + +/// `phase` in `-1..=1`. +#[derive(Debug, Copy, Clone)] +pub struct Moon { + pub phase: f32 +} + +impl BoundingBox for Moon { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::splat(-1.0), + Point2D::splat(1.0) + )}} + +impl SDF for Moon { + fn sdf(&self, pixel: Point2D) -> f32 { + let pixel = V2::<_, WorldSpace>::new(pixel.x, pixel.y.abs()); + let d = self.phase * 2.0; + let a = (d * d) / (2.0 * d); + let b = (1.0 - a * a).max(0.0).sqrt(); + + if d * (pixel.x * b - pixel.y * a) > d * d * (b - pixel.y).max(0.0) { + (pixel - V2::new(a, b)).length() + } else { + (pixel.length() - 1.0).max( + -((pixel - V2::new(d, 0.0)).length() - 1.0) + ) + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Kakera { + pub b: T +} + +impl BoundingBox for Kakera { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::new(-self.b, -1.0), + Point2D::new(self.b, 1.0) + )}} + +impl SDF for Kakera { + fn sdf(&self, pixel: Point2D) -> f32 { + let ndot = |a: V2<_, _,>, b: V2<_, _,>| a.x*b.x - a.y*b.y; + let b = V2::new(self.b, 1.0); + let q = pixel.to_vector().abs(); + let h = ((-2.0 * ndot(q, b) + ndot(b, b)) / b.dot(b)) + .clamp(-1.0, 1.0); + let d = (q - V2::new(1.0 - h, 1.0 + h).component_mul(b) * 0.5).length(); + d * (q.x * b.y + q.y * b.x - b.x * b.y).signum() + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Cross { + pub thickness: T +} + +impl BoundingBox for Cross { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::splat(-1.0), + Point2D::splat(1.0) + )}} + +impl SDF for Cross { + fn sdf(&self, pixel: Point2D) -> f32 { + let mut pixel = pixel.to_vector().abs(); + pixel = if pixel.y > pixel.x { pixel.yx() } else { pixel }; + let q = pixel - V2::new(1.0, self.thickness); + let k = q.x.max(q.y); + let w = if k > 0.0 { q } else { V2::new(self.thickness - pixel.x, -k) }; + k.signum() * w.max(V2::splat(0.0)).length() } } + +#[derive(Debug, Copy, Clone)] +pub struct Ring { + pub inner_r: T +} + +impl BoundingBox for Ring { + fn bounding_box(&self) -> Box2D { + Box2D::new( + Point2D::splat(-1.0), + Point2D::splat(1.0) + )}} + +impl SDF for Ring { + fn sdf(&self, pixel: Point2D) -> f32 { + Circle.subtraction(Circle.scale(V2::splat(self.inner_r))).sdf(pixel) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Polygon { + pub vertices: T +} + +impl BoundingBox for Polygon + where T: AsRef<[Point2D]> { + fn bounding_box(&self) -> Box2D { + Box2D::from_points(self.vertices.as_ref()) + }} + +impl SDF for Polygon + where T: AsRef<[Point2D]> { + fn sdf(&self, pixel: Point2D) -> f32 { + let v = self.vertices.as_ref(); + let mut d = match v.get(0) { + Some(&v) => (pixel - v).dot(pixel - v), + None => return f32::MAX / 2.0 + }; + let mut s = 1.0; + let n = v.len(); + (0..n).zip(std::iter::once(n - 1).chain(0..n - 1)) + .for_each(|(i, j)| { + let e = v[j] - v[i]; + let w = pixel - v[i]; + let b = w - e * (w.dot(e) / e.dot(e)).clamp(0.0, 1.0); + d = d.min(b.dot(b)); + let c = euclid::BoolVector3D { + x: pixel.y >= v[i].y, + y: pixel.y < v[j].y, + z: e.x * w.y > e.y * w.x + }; + if c.all() || c.none() { + s *= -1.0; + } + }); + s * d.sqrt() + } +} + +/// `= NGonC::<3>` +pub type Triangle = NGonC::<3>; + +/// `= NGonC::<5>` +pub type Pentagon = NGonC::<5>; + +/// `= NGonC::<6>` +pub type Hexagon = NGonC::<6>; + +/// `= NGonC::<7>` +pub type Heptagon = NGonC::<7>; + +/// `= NGonC::<8>` +pub type Octagon = NGonC::<8>; + +/// `= Rect { size: [2.0, 2.0] }` +pub static Square: Rect = Rect { + size: Point2D { x: 2.0, y: 2.0, _unit: PhantomData:: } +}; + +/// `= Star { n: 5, m: 10.0 / 3.0 }` +pub static Pentagram: Star = Star { n: 5, m: 10.0 / 3.0 }; + +/// `= Star { n: 6, m: 3.0 }` +pub static Hexagram: Star = Star { n: 6, m: 3.0 }; + +pub static HolyCross: Union < + Rect , + Translation < Rect, f32 > +> = Union { + s1: Rect { size: Point2D { x: 0.4, y: 2.0, _unit: PhantomData:: } }, + s2: Translation { + shape: Rect { size: Point2D { x: 1.432, y: 0.4, _unit: PhantomData:: } }, + offset: V2 { x: 0.0, y: -0.3, _unit: PhantomData:: } + } +}; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index a23afb7..fe63ca1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,7 @@ //! ```ignore //! let shapes: Vec> = vec![ //! Box::new(Circle.translate(...).scale(...)), -//! Box::new(Rect.translate(...).scale(...)) +//! Box::new(Square.translate(...).scale(...)) //! ]; //! for shape in shapes { //! shape.texture(...) diff --git a/src/sdf.rs b/src/sdf.rs index 182320a..e4583ef 100644 --- a/src/sdf.rs +++ b/src/sdf.rs @@ -1,6 +1,7 @@ use { - euclid::{Point2D, Vector2D as V2, Rotation2D}, - crate::geometry::{self, WorldSpace, Shape, Rotation, Scale, Translation} + euclid::{Point2D, Vector2D as V2, Rotation2D, Box2D}, + crate::geometry::{self, WorldSpace, Shape, Rotation, Scale, Translation, BoundingBox}, + num_traits::Float }; /// Signed distance function @@ -45,3 +46,103 @@ pub fn boundary_rect(pixel: Point2D) -> f32 { .scale(V2::splat(0.5)) .sdf(pixel) } + +/// Union of two SDFs. +#[derive(Clone, Copy, Debug)] +pub struct Union { + pub s1: S1, + pub s2: S2, +} + +impl SDF for Union + where T: Float, + S1: SDF, + S2: SDF { + fn sdf(&self, pixel: Point2D) -> T { + self.s1.sdf(pixel).min(self.s2.sdf(pixel)) + }} + +impl BoundingBox for Union + where T: Copy + PartialOrd, + S1: BoundingBox, + S2: BoundingBox { + fn bounding_box(&self) -> Box2D { + self.s1.bounding_box().union(&self.s2.bounding_box()) + }} + +/// Subtracion of two SDFs. Note that this operation is *not* commutative, +/// i.e. `Subtraction {a, b} =/= Subtraction {b, a}`. +#[derive(Clone, Copy, Debug)] +pub struct Subtraction { + pub s1: S1, + pub s2: S2, +} + +impl SDF for Subtraction + where T: Float, + S1: SDF, + S2: SDF { + fn sdf(&self, pixel: Point2D) -> T { + (-self.s2.sdf(pixel)).max(self.s1.sdf(pixel)) + }} + +impl BoundingBox for Subtraction + where T: Copy + PartialOrd, + S1: BoundingBox, + S2: BoundingBox { + fn bounding_box(&self) -> Box2D { + self.s1.bounding_box().union(&self.s2.bounding_box()) + }} + +/// Intersection of two SDFs. +#[derive(Clone, Copy, Debug)] +pub struct Intersection { + pub s1: S1, + pub s2: S2, +} + +impl SDF for Intersection + where T: Float, + S1: SDF, + S2: SDF { + fn sdf(&self, pixel: Point2D) -> T { + self.s1.sdf(pixel).max(self.s2.sdf(pixel)) + }} + +impl BoundingBox for Intersection + where T: Copy + PartialOrd + num_traits::Zero, + S1: BoundingBox, + S2: BoundingBox { + fn bounding_box(&self) -> Box2D { + self.s1.bounding_box() + .intersection(&self.s2.bounding_box()) + .unwrap_or(Box2D::from_size([T::zero(), T::zero()].into())) + }} + +/// Takes the minimum of two SDFs, smoothing between them when they are close. +/// +/// `k` controls the radius/distance of the smoothing. 32 is a good default value. +#[derive(Clone, Copy, Debug)] +pub struct SmoothMin { + pub s1: S1, + pub s2: S2, + pub k: T +} + +impl SDF for SmoothMin + where T: Float, + S1: SDF, + S2: SDF { + fn sdf(&self, pixel: Point2D) -> T { + let (s1, s2) = (self.s1.sdf(pixel), self.s2.sdf(pixel)); + let res = (-self.k * s1).exp2() + (-self.k * s2).exp2(); + -res.log2() / self.k + }} + +impl BoundingBox for SmoothMin + where T: Copy + PartialOrd, + S1: BoundingBox, + S2: BoundingBox { + fn bounding_box(&self) -> Box2D { + self.s1.bounding_box().union(&self.s2.bounding_box()) + }} \ No newline at end of file