From 35bfaedf31b5e6d1c89c8235730804254222d8a6 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 08:59:27 -0400 Subject: [PATCH 1/6] More handy workspace setup. --- .gitignore | 8 ++++++++ .vscode/settings.json | 1 + 2 files changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 4fffb2f8..0db77a67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ +# Rust /target /Cargo.lock + +# Inochi2D project/model file +*.inp +*.inx + +# renderdoc capture file +*.cap diff --git a/.vscode/settings.json b/.vscode/settings.json index c8a8b11f..6a43b556 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "no-inline-html": false, "first-line-heading": false }, + "cSpell.enabled": false, // "rust-analyzer.cargo.target": "wasm32-unknown-unknown" } \ No newline at end of file From f6811a61241bab44616dcc883f145a09d97dbc64 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 09:01:37 -0400 Subject: [PATCH 2/6] Physics param map mode `YX`. --- inox2d/src/formats/payload.rs | 1 + inox2d/src/node/components.rs | 1 + inox2d/src/physics/pendulum.rs | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index dc8d041f..887e40b8 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -157,6 +157,7 @@ fn deserialize_simple_physics(obj: JsonObject) -> InoxParseResult map_mode: match obj.get_str("map_mode")? { "AngleLength" => PhysicsParamMapMode::AngleLength, "XY" => PhysicsParamMapMode::XY, + "YX" => PhysicsParamMapMode::YX, unknown => return Err(InoxParseError::UnknownParamMapMode(unknown.to_owned())), }, diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index 7d1c2521..7f77be89 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -130,6 +130,7 @@ pub enum PhysicsModel { pub enum PhysicsParamMapMode { AngleLength, XY, + YX, } #[derive(Clone)] diff --git a/inox2d/src/physics/pendulum.rs b/inox2d/src/physics/pendulum.rs index 6228b5a1..5c346bdb 100644 --- a/inox2d/src/physics/pendulum.rs +++ b/inox2d/src/physics/pendulum.rs @@ -62,6 +62,14 @@ impl SimplePhysicsCtx for T { result.y = -result.y; // Y goes up for params result } + PhysicsParamMapMode::YX => { + let local_pos_norm = local_angle * relative_length; + let mut result = local_pos_norm - Vec2::Y; + result.y = -result.y; // Y goes up for params + + use glam::Vec2Swizzles; + result.yx() + } PhysicsParamMapMode::AngleLength => { let a = f32::atan2(-local_angle.x, local_angle.y) / PI; Vec2::new(a, relative_length) From ac500323979bb0f5f3f1c31699401f5216453045 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 09:03:11 -0400 Subject: [PATCH 3/6] Dummy deser for param binding of `opacity`. To load Grillo's model properly. --- inox2d/src/formats/payload.rs | 2 ++ inox2d/src/params.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 887e40b8..7d310bef 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -455,6 +455,8 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar BindingValues::Deform(Matrix2d::from_slice_vecs(&parsed, true)?) } + // TODO + "opacity" => BindingValues::Opacity, param_name => return Err(InoxParseError::UnknownParamName(param_name.to_owned())), }) } diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index a7727aeb..9219a1c9 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -32,6 +32,8 @@ pub enum BindingValues { TransformRY(Matrix2d), TransformRZ(Matrix2d), Deform(Matrix2d>), + // TODO + Opacity, } #[derive(Debug, Clone)] @@ -219,6 +221,8 @@ impl Param { .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } + // TODO + BindingValues::Opacity => {} } } } From 6a5663473cc9c33650ef38994f7e10836128fc8e Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 09:04:35 -0400 Subject: [PATCH 4/6] Whitespaces. --- inox2d/src/params.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 9219a1c9..2b17a91d 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -201,19 +201,19 @@ impl Param { .expect("Deform param target must have an associated Mesh."); let vert_len = mesh.vertices.len(); - let mut direct_deform: Vec = Vec::with_capacity(vert_len); - direct_deform.resize(vert_len, Vec2::ZERO); - - bi_interpolate_vec2s_additive( - val_normed, - range_in, - out_top, - out_bottom, - binding.interpolate_mode, - &mut direct_deform, - ); - - direct_deform + let mut direct_deform: Vec = Vec::with_capacity(vert_len); + direct_deform.resize(vert_len, Vec2::ZERO); + + bi_interpolate_vec2s_additive( + val_normed, + range_in, + out_top, + out_bottom, + binding.interpolate_mode, + &mut direct_deform, + ); + + direct_deform }; comps From 2c05a25b0e57275628e1f6a7292bbc55acd4fd5b Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 09:13:59 -0400 Subject: [PATCH 5/6] Meshgroup definition and deserialization. --- inox2d/src/formats/payload.rs | 14 ++++++++++++++ inox2d/src/node/components.rs | 11 +++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 7d310bef..e7026c25 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -244,6 +244,15 @@ fn deserialize_transform(obj: JsonObject) -> InoxParseResult { }) } +fn deserialize_meshgroup(obj: JsonObject) -> InoxParseResult { + Ok(MeshGroup { + dynamic: obj.get_bool("dynamic_deformation")?, + translate_children: obj.get_bool("translate_children")?, + }) +} + +// deserialization helpers + fn deserialize_f32s(val: &[json::JsonValue]) -> Vec { val.iter().filter_map(JsonValue::as_f32).collect::>() } @@ -334,6 +343,11 @@ impl Puppet { "SimplePhysics" => { self.node_comps.add(id, deserialize_simple_physics(data)?); } + "MeshGroup" => { + self.node_comps.add(id, deserialize_meshgroup(data)?); + self.node_comps + .add(id, vals("mesh", deserialize_mesh(data.get_object("mesh")?))?); + } custom => { if let Some(func) = load_node_data_custom { func(self, custom, data)? diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index 7f77be89..f60925b4 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -4,6 +4,7 @@ Inochi2D node types to Inox2D components: - Part -> Drawable + TexturedMesh + Mesh - Composite -> Drawable + Composite - SimplePhysics -> SimplePhysics +- MeshGroup -> MeshGroup + Mesh - Custom nodes by inheritance -> Custom nodes by composition */ @@ -197,14 +198,20 @@ pub struct Mesh { pub origin: Vec2, } +/* --- MESHGROUP --- */ + +pub struct MeshGroup { + pub dynamic: bool, + pub translate_children: bool, +} + /* --- DEFORM STACK --- */ /// Source of a deform. #[derive(Hash, PartialEq, Eq, Copy, Clone)] -#[allow(unused)] pub(crate) enum DeformSource { Param(ParamUuid), - Node(InoxNodeUuid), + MeshGroup(InoxNodeUuid), } /// Internal component solving for deforms of a node. From 7fa2acfba7824c078f55ed5eee967a4aeb0757cf Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 30 Sep 2024 09:35:26 -0400 Subject: [PATCH 6/6] 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 952b312d..f714e2c4 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 00000000..b875ef0b --- /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); + } +}