From b478f61eafc9dd72a5ff984c6e6b832e45527456 Mon Sep 17 00:00:00 2001 From: mikeyo98 Date: Wed, 29 Jul 2026 12:43:23 -0400 Subject: [PATCH 1/4] unnested(?) meshgroup with dynamic deform (nested not supported yet) - a safety check on binding range to handle fails on 1d params - fixed a string parsing problem I sometimes get --- inox2d/src/formats/payload.rs | 6 +- inox2d/src/math/deform.rs | 4 + inox2d/src/params.rs | 157 ++++++++++++++++++++++++------ inox2d/src/puppet.rs | 4 +- inox2d/src/render.rs | 48 ++++++++- inox2d/src/render/deform_stack.rs | 148 ++++++++++++++++++++++++---- 6 files changed, 307 insertions(+), 60 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index c8c122f9..8acf7e07 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -91,7 +91,7 @@ fn deserialize_node(obj: JsonObject) -> InoxParseResult { Ok(ParsedNode { node: InoxNode { uuid: InoxNodeUuid(obj.get_u32("uuid")?), - name: obj.get_str("name")?.to_owned(), + name: obj.get_str("name")?.trim_end_matches('\0').to_owned(), enabled: obj.get_bool("enabled")?, zsort: obj.get_f32("zsort")?, trans_offset: vals("transform", deserialize_transform(obj.get_object("transform")?))?, @@ -226,7 +226,7 @@ fn deserialize_mesh(obj: JsonObject) -> InoxParseResult { let uvs = match uvs { Ok(uvs) => uvs, - Err(e) => vec![], + Err(_e) => vec![], }; Ok(Mesh { @@ -415,7 +415,7 @@ fn deserialize_params(vals: &[json::JsonValue]) -> InoxParseResult InoxParseResult<(String, Param)> { - let name = obj.get_str("name")?.to_owned(); + let name = obj.get_str("name")?.trim_end_matches('\0').to_owned(); Ok(( name.clone(), Param { diff --git a/inox2d/src/math/deform.rs b/inox2d/src/math/deform.rs index 5d08fd2e..dbfc1ed5 100644 --- a/inox2d/src/math/deform.rs +++ b/inox2d/src/math/deform.rs @@ -1,10 +1,14 @@ use glam::{Mat2, Vec2}; +use crate::node::InoxNodeUuid; + /// Different kinds of deform. // TODO: Meshgroup. pub(crate) enum Deform { /// Specifying a displacement for every vertex. Direct(Vec), + /// MESHGROUP_ATTEMPT #1 + FromMeshGroup(Vec, InoxNodeUuid), } /// Element-wise add direct deforms up and write result. diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 2b17a91d..dd80fcf0 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -8,10 +8,10 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{DeformSource, DeformStack, Mesh, TransformStore, ZSort}, + components::{DeformSource, DeformStack, Mesh, MeshGroup, TransformStore, ZSort}, InoxNodeUuid, }; -use crate::puppet::{Puppet, World}; +use crate::puppet::{InoxNodeTree, Puppet, World}; /// Parameter binding to a node. This allows to animate a node based on the value of the parameter that owns it. pub struct Binding { @@ -75,7 +75,7 @@ impl Param { /// /// End users may repeatedly apply a same parameter for multiple times in between frames, /// but other facilities should be present to make sure this `apply()` is only called once per parameter. - pub(crate) fn apply(&self, val: Vec2, comps: &mut World) { + pub(crate) fn apply(&self, val: Vec2, nodes: &InoxNodeTree, comps: &mut World) { let val = val.clamp(self.min, self.max); let val_normed = (val - self.min) / (self.max - self.min); @@ -108,12 +108,19 @@ impl Param { // Apply offset on each binding for binding in &self.bindings { - let range_in = InterpRange::new( + let mut range_in = InterpRange::new( vec2(self.axis_points.x[x_mindex], self.axis_points.y[y_mindex]), vec2(self.axis_points.x[x_maxdex], self.axis_points.y[y_maxdex]), ); let val_normed = val_normed.clamp(range_in.beg, range_in.end); + // Safety check: Avoid division by zero in interpolation + if (range_in.end.x - range_in.beg.x).abs() < 1e-6 { + range_in.end.x += 1.0; + } + if (range_in.end.y - range_in.beg.y).abs() < 1e-6 { + range_in.end.y += 1.0; + } match binding.values { BindingValues::ZSort(ref matrix) => { @@ -194,32 +201,83 @@ impl Param { matrix[(x_maxdex, y_maxdex)].as_slice(), ); - // deform specified by a parameter must be direct, i.e., in the form of displacements of all vertices - let direct_deform = { - let mesh = comps - .get::(binding.node) - .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 - }; - - comps - .get_mut::(binding.node) - .expect("Nodes being deformed must have a DeformStack component.") - .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); + // case Meshgroup + if comps.get::(binding.node).is_some() { + let direct_deform = { + let mesh = comps.get::(binding.node).unwrap_or_else(|| { + panic!( + "Deform param target must have an associated Mesh. (Binding Node ID: {:?})", + binding.node.0 + ) + }); + + 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 is the vec of mesh points' new relative + // coordinates to their origin (in the test example they + // are points on the square mesh) + direct_deform + }; + // It's pushed whenever a deform binding of this node is found + // can we put descendent to the deform stack with + comps + .get_mut::(binding.node) + .expect("Nodes being deformed must have a DeformStack component.") + .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform.clone())); + // For each meshed descendent, push with DeformSource::MeshGroup(), Deform::FromMeshGroup() + // and then later apply with a different combine + if comps.get::(binding.node).unwrap().dynamic { + push_children( + nodes, + comps, + &direct_deform, + binding.node, + binding.node, + // TransformOffset::default().to_matrix(), // Can't use abs transform because bindings may be applied + val, + ); + } + } else { + // deform specified by a parameter must be direct, i.e., in the form of displacements of all vertices + let direct_deform = { + let mesh = comps.get::(binding.node).unwrap_or_else(|| { + panic!( + "Deform param target must have an associated Mesh. (Binding Node ID: {:?})", + binding.node.0 + ) + }); + + 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 + }; + + comps + .get_mut::(binding.node) + .expect("Nodes being deformed must have a DeformStack component.") + .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); + } } // TODO BindingValues::Opacity => {} @@ -228,6 +286,41 @@ impl Param { } } +fn push_children( + nodes: &InoxNodeTree, + comps: &mut World, + meshgroup_deform: &Vec, + meshgroup_uuid: InoxNodeUuid, + parent_uuid: InoxNodeUuid, + val: Vec2, +) { + for child in nodes.get_children(parent_uuid) { + if comps.get::(child.uuid).is_some() { + // TODO: how nested meshgroup works: + // Meshgroup A and its descendent Meshgroup B + // Meshgroup B's mesh is affected by Meshgroup A's deform + // children of meshgroup B gets deform computed from it, NOT meshgroup A + // Therefore, order of applying deform + // = the deform of children of mgB + // = children's own deform + deform computed from mgB + // = children's own deform + (mgB's own deform + deform for mgB computed from mgA) + // + todo!("Nested MeshGroup detected"); + } + // Forgot to put translation of each node to its parent + // the engine only uses relative position to parent for location + + if comps.get::(child.uuid).is_some() { + comps.get_mut::(child.uuid).unwrap().push( + DeformSource::MeshGroup(meshgroup_uuid), + Deform::FromMeshGroup(meshgroup_deform.to_vec(), child.uuid), + ); + } + // don't forget to push descendents recursively + push_children(nodes, comps, meshgroup_deform, meshgroup_uuid, child.uuid, val); + } +} + /// Additional struct attached to a puppet for animating through params. pub struct ParamCtx { values: HashMap, @@ -258,12 +351,12 @@ impl ParamCtx { } /// Modify components as specified by all params. Must be called ONCE per frame. - pub(crate) fn apply(&self, params: &HashMap, comps: &mut World) { + pub(crate) fn apply(&self, params: &HashMap, nodes: &InoxNodeTree, comps: &mut World) { // a correct implementation should not care about the order of `.apply()` for (param_name, val) in self.values.iter() { // TODO: a correct implementation should not fail on param value (0, 0) if *val != Vec2::ZERO { - params.get(param_name).unwrap().apply(*val, comps); + params.get(param_name).unwrap().apply(*val, nodes, comps); } } } diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index ce67ff8d..7cdcbfa1 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -126,7 +126,7 @@ impl Puppet { /// Provide elapsed time for physics, if initialized, to run. Provide `0` for the first call. pub fn end_frame(&mut self, dt: f32) { if let Some(param_ctx) = self.param_ctx.as_mut() { - param_ctx.apply(&self.params, &mut self.node_comps); + param_ctx.apply(&self.params, &self.nodes, &mut self.node_comps); } if let Some(transform_ctx) = self.transform_ctx.as_mut() { @@ -158,7 +158,7 @@ impl Puppet { .set(param_name, *value) .expect("Param name returned by .step() must exist."); } - param_ctx.apply(&self.params, &mut self.node_comps); + param_ctx.apply(&self.params, &self.nodes, &mut self.node_comps); transform_ctx.update(&self.nodes, &mut self.node_comps); } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index d78a236f..90c380e9 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -48,14 +48,40 @@ impl RenderCtx { let comps = &mut puppet.node_comps; let mut nodes_to_deform = HashSet::new(); + // BUG: 2.meshgroup's children don't appear inside the following iteration, when dynamic is on. + // Instead, the meshgroup itself is in bindings. + // This tells us, the static method translate the deform of the meshgroup node to its descendents + // whereas the dynamic method keeps the deform in the meshgroup and compute deform of descendents + // at render time + // I think, static method is equivalent to a textured part controls its children + fn insert_children( + nodes: &InoxNodeTree, + comps: &World, + parent: InoxNodeUuid, + node_set: &mut HashSet, + ) { + for child in nodes.get_children(parent) { + if comps.get::(child.uuid).is_some() { + node_set.insert(child.uuid); + insert_children(nodes, comps, child.uuid, node_set); + } + } + } + + // TODO: Refactoring and nested meshgroup. Putting this into the below loop would be better for param in &puppet.params { param.1.bindings.iter().for_each(|b| { if matches!(b.values, BindingValues::Deform(_)) { nodes_to_deform.insert(b.node); + // TODO: register textured mesh parts of meshgroup node when dynamic is on + // TODO: what is translate children?? + + if comps.get::(b.node).is_some() { + insert_children(nodes, comps, b.node, &mut nodes_to_deform); + } } }); } - // TODO: Further fill the set when Meshgroup is implemented. let mut vertex_buffers = VertexBuffers::default(); @@ -111,6 +137,13 @@ impl RenderCtx { } }; } + // BUG: 3. The following doesn't matter (won't cause panic) to + // meshgroup's children when the meshgroup node has dynamic deform + // turned off. But it won't work as expected when the meshgroup + // node has dynamic on, as it never apply the deform to the descendents + // recursively. + // On dynamic mode, children (and all descendents) do not have + // deform data from the meshgroup parent in the inp file. // MeshGroup isn't drawable, but we still need to make sure it // gets a deform stack @@ -193,6 +226,12 @@ impl RenderCtx { } // for TexturedMesh, obtain and write deforms into vertex_buffer DrawableKind::TexturedMesh(..) => { + // BUG: 1.when dynamic deformation is on, meshgroup's children are not pushed on deform stack + // BUG: 4. Observation, it appears all the descendents share meshgroup's deform on top of their own deform + // BUG: 5: Solution: recursively add deform stacks of meshgroup's descendent parts (on setup) + // recursively push to deform stacks (on end_frame, every frame) + // recursively combine (why not during iteration? Yes, we have put them on the deform stack we good to go) + // TODO: Update combine function to calculate children's deform contributed by the meshgroup // A TexturedMesh not having an associated DeformStack means it will not be deformed at all, skip. if let Some(deform_stack) = comps.get::(node.uuid) { let render_ctx = comps.get::(node.uuid).unwrap(); @@ -206,6 +245,8 @@ impl RenderCtx { } } } + + // next we need to know why it breaks the masks/composites } } @@ -333,8 +374,9 @@ impl InoxRendererExt for T { let drawable_kind = DrawableKind::new(*uuid, comps, false) .expect("All children in zsorted_children_list should be a Drawable."); match drawable_kind { - DrawableKind::TexturedMesh(components) => { - self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) + DrawableKind::TexturedMesh(_components) => { + // self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) + self.draw_drawable(as_mask, comps, *uuid) } DrawableKind::Composite { .. } => panic!("Composite inside Composite not allowed."), } diff --git a/inox2d/src/render/deform_stack.rs b/inox2d/src/render/deform_stack.rs index 037a23db..3301d773 100644 --- a/inox2d/src/render/deform_stack.rs +++ b/inox2d/src/render/deform_stack.rs @@ -3,8 +3,8 @@ use std::mem::swap; use glam::Vec2; -use crate::math::deform::{linear_combine, Deform}; -use crate::node::components::{DeformSource, DeformStack}; +use crate::math::deform::{deform_by_parent_triangle, linear_combine, vector_decompose_matrix, Deform}; +use crate::node::components::{DeformSource, DeformStack, Mesh, TransformStore}; use crate::puppet::{InoxNodeTree, World}; impl DeformStack { @@ -23,39 +23,147 @@ impl DeformStack { } /// Combine the deformations received so far according to some rules, and write to the result - pub(crate) fn combine(&self, _nodes: &InoxNodeTree, _node_comps: &World, result: &mut [Vec2]) { + pub(crate) fn combine(&self, nodes: &InoxNodeTree, node_comps: &World, result: &mut [Vec2]) { if result.len() != self.deform_len { panic!("Required output deform dimensions different from what DeformStack is initialized with.") } - let direct_deforms = self.stack.values().filter_map(|enabled_deform| { + // Single pass might require more structure changes. I can't figure it out how without. + // I will try to make it work as straightforward as possible. + + // Temporary. Nested meshgroup for todo + let mut maybe_meshgroup_uuid = None; + let mut maybe_meshgroup_deform = None; + let mut maybe_node_id = None; + + let direct_deforms = self.stack.iter().filter_map(|(deform_source, enabled_deform)| { if enabled_deform.0 { - let Deform::Direct(ref direct_deform) = enabled_deform.1; - Some(direct_deform) + match (&deform_source, &enabled_deform.1) { + (DeformSource::Param(_), Deform::Direct(ref direct_deform)) => Some(direct_deform), + (DeformSource::MeshGroup(mg_uuid), Deform::FromMeshGroup(ref mg_deform, ref node_uuid)) => { + maybe_meshgroup_uuid = Some(mg_uuid); + maybe_meshgroup_deform = Some(mg_deform); + maybe_node_id = Some(node_uuid); + None + } + _ => todo!(), // panic? It's illegal + } } else { None } }); linear_combine(direct_deforms, result); + + if let (Some(node_id), Some(meshgroup_uuid), Some(meshgroup_deform)) = + (maybe_node_id, maybe_meshgroup_uuid, maybe_meshgroup_deform) + { + let _child_node = nodes.get_node(*node_id).unwrap(); + let child_mesh = node_comps.get::(*node_id).unwrap(); + let child_init_verts = &child_mesh.vertices; // must have a mesh if already on deform stack + let child_direct_verts = child_init_verts + .iter() + .zip(result.iter()) + .map(|(point, deform)| point + deform); + + let child_transform = node_comps.get::(*node_id).unwrap().absolute; + // Need this because triangle test is in meshgroup's space + let meshgroup_transform = node_comps.get::(*meshgroup_uuid).unwrap().absolute; + let to_meshgroup_space = meshgroup_transform.inverse() * child_transform; + + // take account of child deform results + // the "result" so far should be applied on the initial mesh, not the transformed mesh + let child_meshgroup_verts: &Vec = &child_direct_verts + .map(|point| to_meshgroup_space.transform_point3(point.extend(0.0)).truncate()) + .collect(); + + // child_verts appears to be incorrect + // it should include its own deform + + let meshgroup_mesh = node_comps.get::(*meshgroup_uuid).unwrap(); + // // TODO: bitmask optimization + let triangle_by_point: Vec> = meshgroup_mesh.test(child_meshgroup_verts.iter()).collect(); + + let mut deform_results = vec![Vec2::default(); child_meshgroup_verts.len()]; + let mut grouped_points: HashMap, Vec<&Vec2>)> = HashMap::new(); + + for ((idx, point), tri_idx) in child_meshgroup_verts + .iter() + .enumerate() + .zip(triangle_by_point.into_iter()) + { + if let Some(id) = tri_idx { + let (indices, pts) = grouped_points.entry(id).or_default(); + indices.push(idx); + pts.push(point); + } + } + + for (tri_idx, (indices, points_in_tri)) in grouped_points { + let tri = meshgroup_mesh.get_triangle(tri_idx); + let decompose_matrix = vector_decompose_matrix(tri[1] - tri[0], tri[2] - tri[0]); + let parent_deforms = [ + meshgroup_deform[meshgroup_mesh.indices[3 * tri_idx as usize] as usize], + meshgroup_deform[meshgroup_mesh.indices[(3 * tri_idx + 1) as usize] as usize], + meshgroup_deform[meshgroup_mesh.indices[(3 * tri_idx + 2) as usize] as usize], + ]; + + let deform_by_triangle = + deform_by_parent_triangle(&decompose_matrix, tri[0], &parent_deforms, points_in_tri.into_iter()); + + for (idx, deform_point) in indices.into_iter().zip(deform_by_triangle) { + deform_results[idx] = deform_point; + } + } + // linear_combine(vec![deform_results].iter(), result); + + // The calculated deform is in meshgroup's space, + // so convert the deform back to the target mesh's space + let to_node_space = child_transform.inverse() * meshgroup_transform; + let deform_node_space = deform_results + .iter() + .map(|deform| to_node_space.transform_vector3(deform.extend(0.0)).truncate()); + + result + .iter_mut() + .zip(deform_node_space) + .for_each(|(sum, addition)| *sum += addition); + } } /// Submit a deform from a source for a node. pub(crate) fn push(&mut self, src: DeformSource, mut deform: Deform) { - let Deform::Direct(ref direct_deform) = deform; - if direct_deform.len() != self.deform_len { - panic!("A direct deform with non-matching dimensions is submitted to a node."); - } - - self.stack - .entry(src) - .and_modify(|enabled_deform| { - if enabled_deform.0 { - panic!("A same source submitted deform twice for a same node within one frame.") + match deform { + Deform::Direct(ref direct_deform) => { + if direct_deform.len() != self.deform_len { + panic!("A direct deform with non-matching dimensions is submitted to a node."); } - enabled_deform.0 = true; - swap(&mut enabled_deform.1, &mut deform); - }) - .or_insert((true, deform)); + self.stack + .entry(src) + .and_modify(|enabled_deform| { + if enabled_deform.0 { + panic!("A same source submitted deform twice for a same node within one frame.") + } + enabled_deform.0 = true; + + swap(&mut enabled_deform.1, &mut deform); + }) + .or_insert((true, deform)); + } + // TODO: I don't know if we can add necessary information so we can use them during combine + Deform::FromMeshGroup(_, _) => { + self.stack + .entry(src) + .and_modify(|enabled_deform| { + if enabled_deform.0 { + panic!("A same source submitted deform twice for a same node within one frame.") + } + enabled_deform.0 = true; + + swap(&mut enabled_deform.1, &mut deform); + }) + .or_insert((true, deform)); + } + } } } From 4ea404cbd1850fab61f471cd2f5917859a3b292b Mon Sep 17 00:00:00 2001 From: mikeyo98 Date: Thu, 30 Jul 2026 07:07:54 -0400 Subject: [PATCH 2/4] Support nested meshgroups --- inox2d/src/node/components.rs | 2 +- inox2d/src/params.rs | 30 ++++++++++-------------------- inox2d/src/render.rs | 7 ++++--- inox2d/src/render/deform_stack.rs | 2 +- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index f60925b4..044d14bd 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -211,7 +211,7 @@ pub struct MeshGroup { #[derive(Hash, PartialEq, Eq, Copy, Clone)] pub(crate) enum DeformSource { Param(ParamUuid), - MeshGroup(InoxNodeUuid), + MeshGroup(ParamUuid, InoxNodeUuid), } /// Internal component solving for deforms of a node. diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index dd80fcf0..1f5f3453 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -237,15 +237,7 @@ impl Param { // For each meshed descendent, push with DeformSource::MeshGroup(), Deform::FromMeshGroup() // and then later apply with a different combine if comps.get::(binding.node).unwrap().dynamic { - push_children( - nodes, - comps, - &direct_deform, - binding.node, - binding.node, - // TransformOffset::default().to_matrix(), // Can't use abs transform because bindings may be applied - val, - ); + push_children(nodes, comps, self.uuid, &direct_deform, binding.node, binding.node); } } else { // deform specified by a parameter must be direct, i.e., in the form of displacements of all vertices @@ -289,35 +281,33 @@ impl Param { fn push_children( nodes: &InoxNodeTree, comps: &mut World, + param_uuid: ParamUuid, meshgroup_deform: &Vec, meshgroup_uuid: InoxNodeUuid, parent_uuid: InoxNodeUuid, - val: Vec2, ) { for child in nodes.get_children(parent_uuid) { if comps.get::(child.uuid).is_some() { - // TODO: how nested meshgroup works: - // Meshgroup A and its descendent Meshgroup B - // Meshgroup B's mesh is affected by Meshgroup A's deform + // TODO: how nested meshgroup works with dynamic off: + // Meshgroup A (dy off) and its descendent Meshgroup B (dy on) + // Meshgroup B's mesh is affected by Meshgroup A's deform (but it's already exported) // children of meshgroup B gets deform computed from it, NOT meshgroup A // Therefore, order of applying deform // = the deform of children of mgB // = children's own deform + deform computed from mgB // = children's own deform + (mgB's own deform + deform for mgB computed from mgA) + // when dynamic on, the descendent meshgroups dont get affected // - todo!("Nested MeshGroup detected"); + // todo!("Nested MeshGroup detected"); + continue; } - // Forgot to put translation of each node to its parent - // the engine only uses relative position to parent for location - if comps.get::(child.uuid).is_some() { comps.get_mut::(child.uuid).unwrap().push( - DeformSource::MeshGroup(meshgroup_uuid), + DeformSource::MeshGroup(param_uuid, meshgroup_uuid), Deform::FromMeshGroup(meshgroup_deform.to_vec(), child.uuid), ); } - // don't forget to push descendents recursively - push_children(nodes, comps, meshgroup_deform, meshgroup_uuid, child.uuid, val); + push_children(nodes, comps, param_uuid, meshgroup_deform, meshgroup_uuid, child.uuid); } } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 90c380e9..6274d1fb 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -73,11 +73,12 @@ impl RenderCtx { param.1.bindings.iter().for_each(|b| { if matches!(b.values, BindingValues::Deform(_)) { nodes_to_deform.insert(b.node); - // TODO: register textured mesh parts of meshgroup node when dynamic is on // TODO: what is translate children?? - if comps.get::(b.node).is_some() { - insert_children(nodes, comps, b.node, &mut nodes_to_deform); + if let Some(meshgroup) = comps.get::(b.node) { + if meshgroup.dynamic { + insert_children(nodes, comps, b.node, &mut nodes_to_deform); + } } } }); diff --git a/inox2d/src/render/deform_stack.rs b/inox2d/src/render/deform_stack.rs index 3301d773..2b9b10a0 100644 --- a/inox2d/src/render/deform_stack.rs +++ b/inox2d/src/render/deform_stack.rs @@ -40,7 +40,7 @@ impl DeformStack { if enabled_deform.0 { match (&deform_source, &enabled_deform.1) { (DeformSource::Param(_), Deform::Direct(ref direct_deform)) => Some(direct_deform), - (DeformSource::MeshGroup(mg_uuid), Deform::FromMeshGroup(ref mg_deform, ref node_uuid)) => { + (DeformSource::MeshGroup(_, mg_uuid), Deform::FromMeshGroup(ref mg_deform, ref node_uuid)) => { maybe_meshgroup_uuid = Some(mg_uuid); maybe_meshgroup_deform = Some(mg_deform); maybe_node_id = Some(node_uuid); From 09a41f24d18ce36d1bf5f72e9118b9189bbfe009 Mon Sep 17 00:00:00 2001 From: jza221 Date: Sat, 15 Aug 2026 08:10:54 -0700 Subject: [PATCH 3/4] Use bitmask optimization for triangle test and clean up old comments --- inox2d/src/math/deform.rs | 1 - inox2d/src/math/triangle.rs | 10 +++--- inox2d/src/params.rs | 20 ++---------- inox2d/src/render.rs | 26 ++-------------- inox2d/src/render/deform_stack.rs | 52 ++++++++++++++----------------- 5 files changed, 34 insertions(+), 75 deletions(-) diff --git a/inox2d/src/math/deform.rs b/inox2d/src/math/deform.rs index dbfc1ed5..71513313 100644 --- a/inox2d/src/math/deform.rs +++ b/inox2d/src/math/deform.rs @@ -3,7 +3,6 @@ use glam::{Mat2, Vec2}; use crate::node::InoxNodeUuid; /// Different kinds of deform. -// TODO: Meshgroup. pub(crate) enum Deform { /// Specifying a displacement for every vertex. Direct(Vec), diff --git a/inox2d/src/math/triangle.rs b/inox2d/src/math/triangle.rs index b875ef0b..38050fdc 100644 --- a/inox2d/src/math/triangle.rs +++ b/inox2d/src/math/triangle.rs @@ -189,7 +189,7 @@ impl<'mesh> MeshBitMask<'mesh> { } /// Return the index of the triangle point `p` is in, if any. - pub fn test(&self, p: Vec2) -> Option { + pub fn test(&self, p: &Vec2) -> Option { // handle empty mesh case if self.mask.is_empty() { return None; @@ -208,7 +208,7 @@ impl<'mesh> MeshBitMask<'mesh> { candidates .into_iter() - .find(|&t| is_point_in_triangle(p, &self.mesh.get_triangle(t))) + .find(|&t| is_point_in_triangle(*p, &self.mesh.get_triangle(t))) } } @@ -325,7 +325,7 @@ mod tests { test_with_mesh(*transform, |mesh, ps| { let bit_mask = MeshBitMask::new(mesh); - ps.into_iter().map(|p| bit_mask.test(p)).collect() + ps.into_iter().map(|p| bit_mask.test(&p)).collect() }) }) } @@ -342,7 +342,7 @@ mod tests { 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); + assert_eq!(bit_mask.test(&vec2(-1.0, 0.0)), None); + assert_eq!(bit_mask.test(&vec2(1.0, 2.0)), None); } } diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 1f5f3453..394423a6 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -223,19 +223,14 @@ impl Param { binding.interpolate_mode, &mut direct_deform, ); - // direct_deform is the vec of mesh points' new relative - // coordinates to their origin (in the test example they - // are points on the square mesh) direct_deform }; - // It's pushed whenever a deform binding of this node is found - // can we put descendent to the deform stack with comps .get_mut::(binding.node) .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform.clone())); - // For each meshed descendent, push with DeformSource::MeshGroup(), Deform::FromMeshGroup() - // and then later apply with a different combine + + // Push deform data of descendants from meshgroup onto deform stack if comps.get::(binding.node).unwrap().dynamic { push_children(nodes, comps, self.uuid, &direct_deform, binding.node, binding.node); } @@ -288,17 +283,6 @@ fn push_children( ) { for child in nodes.get_children(parent_uuid) { if comps.get::(child.uuid).is_some() { - // TODO: how nested meshgroup works with dynamic off: - // Meshgroup A (dy off) and its descendent Meshgroup B (dy on) - // Meshgroup B's mesh is affected by Meshgroup A's deform (but it's already exported) - // children of meshgroup B gets deform computed from it, NOT meshgroup A - // Therefore, order of applying deform - // = the deform of children of mgB - // = children's own deform + deform computed from mgB - // = children's own deform + (mgB's own deform + deform for mgB computed from mgA) - // when dynamic on, the descendent meshgroups dont get affected - // - // todo!("Nested MeshGroup detected"); continue; } if comps.get::(child.uuid).is_some() { diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 6274d1fb..3635c1cf 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -48,12 +48,7 @@ impl RenderCtx { let comps = &mut puppet.node_comps; let mut nodes_to_deform = HashSet::new(); - // BUG: 2.meshgroup's children don't appear inside the following iteration, when dynamic is on. - // Instead, the meshgroup itself is in bindings. - // This tells us, the static method translate the deform of the meshgroup node to its descendents - // whereas the dynamic method keeps the deform in the meshgroup and compute deform of descendents - // at render time - // I think, static method is equivalent to a textured part controls its children + fn insert_children( nodes: &InoxNodeTree, comps: &World, @@ -68,7 +63,7 @@ impl RenderCtx { } } - // TODO: Refactoring and nested meshgroup. Putting this into the below loop would be better + // TODO: Refactoring and nested meshgroup. Maybe putting this into the below loop would be better? for param in &puppet.params { param.1.bindings.iter().for_each(|b| { if matches!(b.values, BindingValues::Deform(_)) { @@ -138,13 +133,6 @@ impl RenderCtx { } }; } - // BUG: 3. The following doesn't matter (won't cause panic) to - // meshgroup's children when the meshgroup node has dynamic deform - // turned off. But it won't work as expected when the meshgroup - // node has dynamic on, as it never apply the deform to the descendents - // recursively. - // On dynamic mode, children (and all descendents) do not have - // deform data from the meshgroup parent in the inp file. // MeshGroup isn't drawable, but we still need to make sure it // gets a deform stack @@ -227,12 +215,6 @@ impl RenderCtx { } // for TexturedMesh, obtain and write deforms into vertex_buffer DrawableKind::TexturedMesh(..) => { - // BUG: 1.when dynamic deformation is on, meshgroup's children are not pushed on deform stack - // BUG: 4. Observation, it appears all the descendents share meshgroup's deform on top of their own deform - // BUG: 5: Solution: recursively add deform stacks of meshgroup's descendent parts (on setup) - // recursively push to deform stacks (on end_frame, every frame) - // recursively combine (why not during iteration? Yes, we have put them on the deform stack we good to go) - // TODO: Update combine function to calculate children's deform contributed by the meshgroup // A TexturedMesh not having an associated DeformStack means it will not be deformed at all, skip. if let Some(deform_stack) = comps.get::(node.uuid) { let render_ctx = comps.get::(node.uuid).unwrap(); @@ -246,8 +228,6 @@ impl RenderCtx { } } } - - // next we need to know why it breaks the masks/composites } } @@ -375,7 +355,7 @@ impl InoxRendererExt for T { let drawable_kind = DrawableKind::new(*uuid, comps, false) .expect("All children in zsorted_children_list should be a Drawable."); match drawable_kind { - DrawableKind::TexturedMesh(_components) => { + DrawableKind::TexturedMesh(components) => { // self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) self.draw_drawable(as_mask, comps, *uuid) } diff --git a/inox2d/src/render/deform_stack.rs b/inox2d/src/render/deform_stack.rs index 2b9b10a0..9f75d2ab 100644 --- a/inox2d/src/render/deform_stack.rs +++ b/inox2d/src/render/deform_stack.rs @@ -4,6 +4,7 @@ use std::mem::swap; use glam::Vec2; use crate::math::deform::{deform_by_parent_triangle, linear_combine, vector_decompose_matrix, Deform}; +use crate::math::triangle::MeshBitMask; use crate::node::components::{DeformSource, DeformStack, Mesh, TransformStore}; use crate::puppet::{InoxNodeTree, World}; @@ -29,13 +30,13 @@ impl DeformStack { } // Single pass might require more structure changes. I can't figure it out how without. - // I will try to make it work as straightforward as possible. + // I will try to make it work and look as straightforward as possible. - // Temporary. Nested meshgroup for todo - let mut maybe_meshgroup_uuid = None; - let mut maybe_meshgroup_deform = None; - let mut maybe_node_id = None; + let mut maybe_meshgroup_uuid: Option<&crate::node::InoxNodeUuid> = None; + let mut maybe_meshgroup_deform: Option<&Vec> = None; + let mut maybe_node_id: Option<&crate::node::InoxNodeUuid> = None; + // Apply direct deform first let direct_deforms = self.stack.iter().filter_map(|(deform_source, enabled_deform)| { if enabled_deform.0 { match (&deform_source, &enabled_deform.1) { @@ -46,7 +47,7 @@ impl DeformStack { maybe_node_id = Some(node_uuid); None } - _ => todo!(), // panic? It's illegal + _ => panic!("Unexpected DeformSource and Deform tuple"), } } else { None @@ -57,6 +58,7 @@ impl DeformStack { if let (Some(node_id), Some(meshgroup_uuid), Some(meshgroup_deform)) = (maybe_node_id, maybe_meshgroup_uuid, maybe_meshgroup_deform) { + // Apply transform to meshgroup's space before testing (fast) let _child_node = nodes.get_node(*node_id).unwrap(); let child_mesh = node_comps.get::(*node_id).unwrap(); let child_init_verts = &child_mesh.vertices; // must have a mesh if already on deform stack @@ -72,32 +74,26 @@ impl DeformStack { // take account of child deform results // the "result" so far should be applied on the initial mesh, not the transformed mesh - let child_meshgroup_verts: &Vec = &child_direct_verts - .map(|point| to_meshgroup_space.transform_point3(point.extend(0.0)).truncate()) - .collect(); - - // child_verts appears to be incorrect - // it should include its own deform - - let meshgroup_mesh = node_comps.get::(*meshgroup_uuid).unwrap(); - // // TODO: bitmask optimization - let triangle_by_point: Vec> = meshgroup_mesh.test(child_meshgroup_verts.iter()).collect(); + let child_meshgroup_verts = + child_direct_verts.map(|point| to_meshgroup_space.transform_point3(point.extend(0.0)).truncate()); let mut deform_results = vec![Vec2::default(); child_meshgroup_verts.len()]; - let mut grouped_points: HashMap, Vec<&Vec2>)> = HashMap::new(); + let mut grouped_points: HashMap, Vec)> = HashMap::new(); - for ((idx, point), tri_idx) in child_meshgroup_verts - .iter() - .enumerate() - .zip(triangle_by_point.into_iter()) - { - if let Some(id) = tri_idx { + // Triangle test (takes most time) + // let triangle_by_point = meshgroup_mesh.test(child_meshgroup_verts.iter()); + let meshgroup_mesh = node_comps.get::(*meshgroup_uuid).unwrap(); + let mesh_mask = MeshBitMask::new(meshgroup_mesh); + // TODO: parallel iteration? + child_meshgroup_verts.enumerate().for_each(|(idx, point)| { + if let Some(id) = mesh_mask.test(&point) { let (indices, pts) = grouped_points.entry(id).or_default(); indices.push(idx); pts.push(point); } - } + }); + // Calculate deform by meshgroup (fast) for (tri_idx, (indices, points_in_tri)) in grouped_points { let tri = meshgroup_mesh.get_triangle(tri_idx); let decompose_matrix = vector_decompose_matrix(tri[1] - tri[0], tri[2] - tri[0]); @@ -108,14 +104,14 @@ impl DeformStack { ]; let deform_by_triangle = - deform_by_parent_triangle(&decompose_matrix, tri[0], &parent_deforms, points_in_tri.into_iter()); + deform_by_parent_triangle(&decompose_matrix, tri[0], &parent_deforms, points_in_tri.iter()); - for (idx, deform_point) in indices.into_iter().zip(deform_by_triangle) { - deform_results[idx] = deform_point; + for (idx, deform_point) in indices.iter().zip(deform_by_triangle) { + deform_results[*idx] = deform_point; } } - // linear_combine(vec![deform_results].iter(), result); + // Apply deform by meshgroup (fast) // The calculated deform is in meshgroup's space, // so convert the deform back to the target mesh's space let to_node_space = child_transform.inverse() * meshgroup_transform; From b9799f8fd7ed53bc5ac0a88de269de8128bdff84 Mon Sep 17 00:00:00 2001 From: jza221 Date: Sat, 15 Aug 2026 09:31:29 -0700 Subject: [PATCH 4/4] More old custom code cleanup --- inox2d/src/params.rs | 11 +---------- inox2d/src/render.rs | 3 +-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 394423a6..241362a9 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -108,20 +108,11 @@ impl Param { // Apply offset on each binding for binding in &self.bindings { - let mut range_in = InterpRange::new( + let range_in = InterpRange::new( vec2(self.axis_points.x[x_mindex], self.axis_points.y[y_mindex]), vec2(self.axis_points.x[x_maxdex], self.axis_points.y[y_maxdex]), ); - let val_normed = val_normed.clamp(range_in.beg, range_in.end); - // Safety check: Avoid division by zero in interpolation - if (range_in.end.x - range_in.beg.x).abs() < 1e-6 { - range_in.end.x += 1.0; - } - if (range_in.end.y - range_in.beg.y).abs() < 1e-6 { - range_in.end.y += 1.0; - } - match binding.values { BindingValues::ZSort(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 3635c1cf..a9d66bed 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -356,8 +356,7 @@ impl InoxRendererExt for T { .expect("All children in zsorted_children_list should be a Drawable."); match drawable_kind { DrawableKind::TexturedMesh(components) => { - // self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) - self.draw_drawable(as_mask, comps, *uuid) + self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) } DrawableKind::Composite { .. } => panic!("Composite inside Composite not allowed."), }