From 7fa2acfba7824c078f55ed5eee967a4aeb0757cf Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 09:35:26 -0400 Subject: [PATCH] Math required to implement MeshGroup. Three parts: - Testing whether a point is in a triangle. - Efficient (but not guaranteed correct) testing of a point is in which triangle among those in a given mesh, with a bitmask constructed for the mesh. - Given parent mesh and parent deformation, deform child mesh. Essentially letting one parent triangle drag a child vertex that is contained in it, and this drag can be chacterized by an inverse matrix, which can be reused for efficiency. Intensive test for bitmask included (with a .png depicting the base test case), testing that bitmask yields correct test results for points with the whole test space transforming. However, as mentioned, this bitmask method is not guaranteed to be correct, thus it is hard to define a "good enough" test case. It is easy to construct counterexamples. An experimental attempt on dynamic bitmask step size is commented out cause test suites do not like it. A step size of `1` should be usable in actual rendering anyways. --- inox2d/src/math.rs | 1 + inox2d/src/math/bit_mask_test.png | Bin 0 -> 12963 bytes inox2d/src/math/deform.rs | 45 +++- inox2d/src/math/triangle.rs | 348 ++++++++++++++++++++++++++++++ 4 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 inox2d/src/math/bit_mask_test.png create mode 100644 inox2d/src/math/triangle.rs diff --git a/inox2d/src/math.rs b/inox2d/src/math.rs index 952b312..f714e2c 100644 --- a/inox2d/src/math.rs +++ b/inox2d/src/math.rs @@ -3,3 +3,4 @@ pub mod deform; pub mod interp; pub mod matrix; pub mod transform; +pub(crate) mod triangle; diff --git a/inox2d/src/math/bit_mask_test.png b/inox2d/src/math/bit_mask_test.png new file mode 100644 index 0000000000000000000000000000000000000000..b52f41a48a3619c9263619c8aa748f159fee85ac GIT binary patch literal 12963 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 cJGes*D))p$X|x^`gX55(direct_deforms: impl Iterator Mat2 { + // B X = V where: + // B: [ b0.x b1.x + // b0.y b1.y ] + // X: [ x + // y ] + // V: [ v.x + // v.y ] + // thus X = B^-1 V + let mat = Mat2::from_cols(b0, b1).inverse(); + debug_assert_ne!(mat.determinant(), 0.0, "Provided two basis do not span the 2D plane."); + mat +} + +/// Provide a parent triangle and its deforms by 3 points, +/// calculate how far should the provided points be moved by the triangle's deform. +/// +/// For optimization, the "decompose_matrix" of parent should be provided, see `vector_decompose_matrix()`. +/// It is assumed that `parent[0]` is taken as the origin, +/// `parent[1] - parent[0]` is the first basis vector, and `parent[2] - parent[0]` the second. +#[inline] +pub fn deform_by_parent_triangle<'a>( + decompose_matrix: &'a Mat2, + parent_p0: Vec2, + parent_deforms: &'a [Vec2; 3], + points: impl Iterator + 'a, +) -> impl Iterator + 'a { + let basis_0_deform = parent_deforms[1] - parent_deforms[0]; + let basis_1_deform = parent_deforms[2] - parent_deforms[0]; + + points.map(move |p| { + let decomposed_coeffs = *decompose_matrix * (*p - parent_p0); + // deform by parent[0] + deform by basis change + parent_deforms[0] + decomposed_coeffs.x * basis_0_deform + decomposed_coeffs.y * basis_1_deform + }) +} diff --git a/inox2d/src/math/triangle.rs b/inox2d/src/math/triangle.rs new file mode 100644 index 0000000..b875ef0 --- /dev/null +++ b/inox2d/src/math/triangle.rs @@ -0,0 +1,348 @@ +use std::num::NonZeroU16; + +use glam::{Mat2, Vec2}; + +use crate::node::components::Mesh; + +/// Undefined if point is exactly on the edge. +/// +/// Though, due to floating point precision it is hard for a point to be exactly on the edge, +/// let alone that for points so close to the edge, whether they are actually in the triangle do not matter too much. +pub fn is_point_in_triangle(p: Vec2, triangle: &[Vec2; 3]) -> bool { + #[inline] + fn sign(p1: Vec2, p2: Vec2, p3: Vec2) -> f32 { + Mat2::from_cols(p1, p2).sub_mat2(&Mat2::from_cols(p3, p3)).determinant() + } + + let p1 = triangle[0]; + let p2 = triangle[1]; + let p3 = triangle[2]; + + let d1 = sign(p, p1, p2); + let d2 = sign(p, p2, p3); + let d3 = sign(p, p3, p1); + + let has_neg = d1.is_sign_negative() || d2.is_sign_negative() || d3.is_sign_negative(); + let has_pos = d1.is_sign_positive() || d2.is_sign_positive() || d3.is_sign_positive(); + + !(has_neg && has_pos) +} + +/// Return top-left and bottom-right corners of the smallest covering rectangle over a list of points. +#[inline] +fn get_bounds<'a>(vertices: impl Iterator) -> (Vec2, Vec2) { + let (mut x, mut y, mut z, mut w) = (f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + vertices.for_each(|v| { + (x, y, z, w) = (x.min(v.x), y.min(v.y), z.max(v.x), w.max(v.y)); + }); + (Vec2::new(x, y), Vec2::new(z, w)) +} + +impl Mesh { + /// The `i`-th triangle as described by `self.indices`. + pub fn get_triangle(&self, i: u16) -> [Vec2; 3] { + [ + self.vertices[self.indices[3 * i as usize] as usize], + self.vertices[self.indices[(3 * i + 1) as usize] as usize], + self.vertices[self.indices[(3 * i + 2) as usize] as usize], + ] + } + + /// Find which triangle of the mesh is a point in, if any, by brute force. + pub fn test<'a>(&'a self, ps: impl Iterator + 'a) -> impl Iterator> + 'a { + ps.map(|p| { + (0..(self.indices.len() / 3) as u16).find(|&i| { + let triangle = self.get_triangle(i); + is_point_in_triangle(*p, &triangle) + }) + }) + } +} + +/// Cache for efficient mesh testing (which triangle is a point in?) +pub struct MeshBitMask<'mesh> { + mesh: &'mesh Mesh, + top_left: Vec2, + // "Grid size" of the mask. The ref impl uses `1.0`. + x_step: f32, + y_step: f32, + width: usize, + height: usize, + /// If `None`, the point belongs to no triangle. + /// Else `Some(i)`, the point belongs to triangle made up of `mesh.indices[3*(i-1):3*i]`. + /// + /// NOTE THE +1 in `i` here! + mask: Vec>, +} + +impl<'mesh> MeshBitMask<'mesh> { + /// Find x coordinate of the nearest grid point on the left. Return boundary value for out-of-bounds input. + #[inline] + fn get_x(&self, x: f32) -> usize { + if x <= self.top_left.x { + 0 + } else { + ((x - self.top_left.x) / self.x_step) + .floor() + .min((self.width - 1) as f32) as usize + } + } + /// Find y coordinate of the nearest grid point on the top. Return boundary value for out-of-bounds input. + #[inline] + fn get_y(&self, y: f32) -> usize { + if y <= self.top_left.y { + 0 + } else { + ((y - self.top_left.y) / self.y_step) + .floor() + .min((self.height - 1) as f32) as usize + } + } + + /// ONLY USE IN `new()` + /// + /// Actually build `mask` content by testing grid points in regions spanned by each triangle. + // Is a method with `&mut self` so that `get_x()` `get_y()` helpers could be reused. + #[inline] + fn build(&mut self) { + self.mask.resize(self.width * self.height, None); + + for i in 0..(self.mesh.indices.len() / 3) as u16 { + let vertices = self.mesh.get_triangle(i); + + let (region_top_left, region_bottom_right) = get_bounds(vertices.iter()); + let x_begin = self.get_x(region_top_left.x); + let x_end = self.get_x(region_bottom_right.x); + let y_begin = self.get_y(region_top_left.y); + let y_end = self.get_y(region_bottom_right.y); + + for x in x_begin..=x_end { + for y in y_begin..=y_end { + let p = self.top_left + Vec2::new(x as f32 * self.x_step, y as f32 * self.y_step); + if is_point_in_triangle(p, &vertices) { + self.mask[x + y * self.width] = Some(NonZeroU16::new(i + 1).unwrap()); + } + } + } + } + } + + /// See `new()`. + const MIN_STEP: f32 = 1.0; + + /// Create a `MeshBitMask` associated to `mesh`, storing a reference to it (thus living as long as `mesh`). + pub fn new(mesh: &'mesh Mesh) -> Self { + let (top_left, bottom_right) = get_bounds(mesh.vertices.iter()); + + // TODO: Figure out if dynamic steps according to mesh are worthy, if so, how to properly do them. + /* + let mut x_step = f32::INFINITY; + let mut y_step = f32::INFINITY; + // If mesh is empty, step will keep being infinity, and this shall not cause problems anyways. + for i in 0..(mesh.indices.len() / 3) as u16 { + let [p0, p1, p2] = mesh.get_triangle(i); + + x_step = x_step + .min(f32::abs(p0.x - p1.x)) + .min(f32::abs(p1.x - p2.x)) + .min(f32::abs(p2.x - p0.x)); + y_step = y_step + .min(f32::abs(p0.y - p1.y)) + .min(f32::abs(p1.y - p2.y)) + .min(f32::abs(p2.y - p0.y)); + + // to prevent step getting too small when badly shaped triangles present + // Yes, this would yield mathematically wrong results if mesh too small, but it is the rigger's problem that they are creating sub-pixel meshes. + if x_step < Self::MIN_STEP { + x_step = Self::MIN_STEP; + tracing::warn!( + "A triangle is too thin on x direction. Testing with this MeshBitMask may not be accurate." + ); + break; + } + if y_step < Self::MIN_STEP { + y_step = Self::MIN_STEP; + tracing::warn!( + "A triangle is too thin on y direction. Testing with this MeshBitMask may not be accurate." + ); + break; + } + } + */ + let x_step = Self::MIN_STEP; + let y_step = Self::MIN_STEP; + + let width = ((bottom_right.x - top_left.x) / x_step).ceil() as usize; + let height = ((bottom_right.y - top_left.y) / y_step).ceil() as usize; + + let mut this = Self { + mesh, + top_left, + x_step, + y_step, + width, + height, + mask: Vec::new(), + }; + this.build(); + this + } + + /// Return the index of the triangle point `p` is in, if any. + pub fn test(&self, p: Vec2) -> Option { + // handle empty mesh case + if self.mask.is_empty() { + return None; + } + + let mut candidates = Vec::with_capacity(9); + for dx in [-self.x_step, 0.0, self.x_step] { + for dy in [-self.y_step, 0.0, self.y_step] { + // x/y out of bounds are already handled in the getters. + if let Some(index_plus_one) = self.mask[self.get_x(p.x + dx) + self.get_y(p.y + dy) * self.width] { + candidates.push(index_plus_one.get() - 1); + } + } + } + candidates.dedup(); + + candidates + .into_iter() + .find(|&t| is_point_in_triangle(p, &self.mesh.get_triangle(t))) + } +} + +#[cfg(test)] +mod tests { + use std::f32::consts::PI; + + use glam::{vec2, Affine2}; + + use super::*; + + /// Run the test function with arbitary affine transforms, which should not change certain properties (e.g. triangle test). + fn test_with_affine(f: impl Fn(&Affine2)) { + for scale in [vec2(1.0, 1.0), vec2(1.0, 3.0), vec2(3.0, 1.0), vec2(7.0, 10.0)] { + for angle in [0.0, PI / 3.0, -PI / 2.0, PI] { + for translation in [vec2(0.0, 0.0), vec2(1.0, 2.0), vec2(-2.0, 1.0), vec2(-2.0, -2.0)] { + let transform = Affine2::from_scale_angle_translation(scale, angle, translation); + f(&transform); + } + } + } + } + + /// Run the test function with mesh(es) and test points and answers, under the given affine transform. + fn test_with_mesh(transform: Affine2, f: impl Fn(&Mesh, Vec) -> Vec>) { + let vertices = vec![ + vec2(2.0, 0.0), + vec2(0.0, 8.0), + vec2(4.0, 2.0), + vec2(8.0, 6.0), + vec2(10.0, 4.0), + ] + .into_iter() + .map(|v| transform.transform_point2(v)) + .collect(); + let indices = vec![0, 1, 2, 0, 2, 4, 1, 2, 3, 2, 3, 4]; + // Invalid mesh struct as `uvs`` is empty, but does not affect testing. + let mesh = Mesh { + vertices, + uvs: Vec::new(), + indices, + origin: Vec2::ZERO, + }; + + let points_and_ans: [(Vec2, Option); 13] = [ + (vec2(-1.0, 0.0), None), + (vec2(5.0, 1.0), None), + (vec2(9.0, 6.0), None), + (vec2(5.0, 7.0), None), + (vec2(100.0, 100.0), None), + (vec2(2.0, 2.0), Some(0)), + (vec2(5.0, 2.0), Some(1)), + (vec2(3.0, 2.0), Some(0)), + (vec2(3.0, 3.0), Some(0)), + (vec2(4.0, 3.0), Some(2)), + (vec2(6.0, 3.0), Some(3)), + (vec2(4.0, 6.0), Some(2)), + (vec2(8.0, 5.0), Some(3)), + ]; + let points: Vec = points_and_ans.iter().map(|p| transform.transform_point2(p.0)).collect(); + let ans: Vec> = points_and_ans.iter().map(|p| p.1).collect(); + + assert_eq!(f(&mesh, points), ans); + } + + #[test] + fn bounds() { + let vertices = [vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(-1.0, -1.0)]; + assert_eq!(get_bounds(vertices.iter()), (glam::vec2(-1.0, -1.0), vec2(1.0, 1.0))) + } + + #[test] + fn triangle() { + let t0 = vec2(0.5, 1.0); + let t1 = vec2(1.0, 0.5); + let t2 = vec2(0.0, 0.0); + let test_xs = Vec::from_iter((0..=3).map(|i| (i as f32) / 3.0)); + let test_ys = Vec::from_iter((1..=5).map(|i| (i as f32) / 5.0)); + let ans = [ + [false, true, false, false], + [false, true, true, false], + [false, true, true, false], + [false, false, true, false], + [false, false, false, false], + ]; + + test_with_affine(|transform| { + let t0 = transform.transform_point2(t0); + let t1 = transform.transform_point2(t1); + let t2 = transform.transform_point2(t2); + let triangle = [t0, t1, t2]; + + for (iy, y) in test_ys.iter().enumerate() { + for (ix, x) in test_xs.iter().enumerate() { + let p = transform.transform_point2(vec2(*x, *y)); + assert_eq!( + is_point_in_triangle(p, &triangle), + ans[iy][ix], + "Triangle: [{t0}, {t1}, {t2}], point: {p}", + ); + } + } + }); + } + + #[test] + fn mesh_test() { + test_with_affine(|transform| test_with_mesh(*transform, |mesh, ps| mesh.test(ps.iter()).collect())) + } + + #[test] + fn bit_mask() { + test_with_affine(|transform| { + test_with_mesh(*transform, |mesh, ps| { + let bit_mask = MeshBitMask::new(mesh); + + ps.into_iter().map(|p| bit_mask.test(p)).collect() + }) + }) + } + + #[test] + fn bit_mask_empty_mesh() { + let mesh = Mesh { + vertices: Vec::new(), + uvs: Vec::new(), + indices: Vec::new(), + origin: Vec2::ZERO, + }; + let bit_mask = MeshBitMask::new(&mesh); + + assert_eq!(bit_mask.width, 0); + assert_eq!(bit_mask.height, 0); + assert_eq!(bit_mask.test(vec2(-1.0, 0.0)), None); + assert_eq!(bit_mask.test(vec2(1.0, 2.0)), None); + } +}