Skip to main content

mittens_engine/engine/ecs/system/
skinned_mesh_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::World;
3use crate::engine::ecs::component::GLTFComponent;
4use crate::engine::ecs::component::RenderableComponent;
5use crate::engine::ecs::component::SkinnedMeshComponent;
6use crate::engine::ecs::component::TransformComponent;
7use crate::engine::ecs::system::{System, TransformSystem};
8use crate::engine::graphics::SkinId;
9use crate::engine::graphics::VisualWorld;
10use crate::engine::graphics::primitives::TransformMatrix;
11use crate::engine::user_input::InputState;
12use std::collections::{HashMap, HashSet};
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16struct BindingKey {
17    mesh_transform: ComponentId,
18    gltf_component: ComponentId,
19    skin_id: SkinId,
20}
21
22/// Computes per-joint skinning matrices for skinned meshes.
23///
24/// This system defers to `TransformSystem` for cached world matrices.
25///
26/// For each `SkinnedMeshComponent`, it computes (mesh-local) skin matrices:
27///
28/// $$ SkinMat_j = inverse(M_meshWorld) * M_jointWorld(j) * IBM(j) $$
29///
30/// so the GPU can skin in mesh-local space, then apply the instance model matrix as usual.
31#[derive(Debug, Default)]
32pub struct SkinnedMeshSystem {
33    // Reverse index so we can mark bindings dirty when a joint transform (or its ancestor) changes.
34    joint_to_bindings: HashMap<ComponentId, Vec<BindingKey>>,
35    // Reverse index so we can mark bindings dirty when the mesh transform (or its ancestor) changes.
36    mesh_transform_to_bindings: HashMap<ComponentId, Vec<BindingKey>>,
37    // Bindings that need recomputation + palette update.
38    dirty_bindings: HashSet<BindingKey>,
39    // Track known bindings so newly spawned rigs get computed once.
40    known_bindings: HashSet<BindingKey>,
41
42    // Per-instance joint resolution for a given (GLTFComponent instance, SkinId).
43    // Stored as Option so we can keep alignment with the skin's joint order even
44    // if a joint node wasn't spawned.
45    instance_joints: HashMap<(ComponentId, SkinId), Vec<Option<ComponentId>>>,
46}
47
48impl SkinnedMeshSystem {
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    pub fn register_skin_instance_joints(
54        &mut self,
55        gltf_component: ComponentId,
56        skin_id: SkinId,
57        joints: Vec<Option<ComponentId>>,
58    ) {
59        self.instance_joints
60            .insert((gltf_component, skin_id), joints);
61
62        // If bindings already exist for this instance+skin, mark them dirty.
63        let affected: Vec<BindingKey> = self
64            .known_bindings
65            .iter()
66            .copied()
67            .filter(|b| b.gltf_component == gltf_component && b.skin_id == skin_id)
68            .collect();
69        for b in affected {
70            self.dirty_bindings.insert(b);
71        }
72    }
73
74    /// Read-only access to the resolved joint transform ComponentIds for a particular
75    /// (GLTFComponent instance, SkinId) pair.
76    ///
77    /// The returned slice is in the same order as `VisualWorld::skin(skin_id).joint_node_indices`.
78    pub fn instance_joints_for_skin(
79        &self,
80        gltf_component: ComponentId,
81        skin_id: SkinId,
82    ) -> Option<&[Option<ComponentId>]> {
83        self.instance_joints
84            .get(&(gltf_component, skin_id))
85            .map(|v| v.as_slice())
86    }
87
88    fn mat4_identity() -> TransformMatrix {
89        [
90            [1.0, 0.0, 0.0, 0.0],
91            [0.0, 1.0, 0.0, 0.0],
92            [0.0, 0.0, 1.0, 0.0],
93            [0.0, 0.0, 0.0, 1.0],
94        ]
95    }
96
97    fn mat4_mul(a: TransformMatrix, b: TransformMatrix) -> TransformMatrix {
98        // Column-major mat4 multiplication: out = a * b.
99        let mut out = [[0.0f32; 4]; 4];
100        for c in 0..4 {
101            for r in 0..4 {
102                out[c][r] =
103                    a[0][r] * b[c][0] + a[1][r] * b[c][1] + a[2][r] * b[c][2] + a[3][r] * b[c][3];
104            }
105        }
106        out
107    }
108
109    fn update_binding(
110        &self,
111        world: &World,
112        visuals: &VisualWorld,
113        binding: BindingKey,
114    ) -> Option<Vec<TransformMatrix>> {
115        let mesh_world = TransformSystem::world_model(world, binding.mesh_transform)
116            .unwrap_or_else(Self::mat4_identity);
117
118        let inv_mesh_world =
119            crate::utils::math::mat4_inverse(mesh_world).unwrap_or_else(Self::mat4_identity);
120
121        let skin = visuals.skin(binding.skin_id)?;
122        let joints = self
123            .instance_joints
124            .get(&(binding.gltf_component, binding.skin_id))?;
125
126        let joint_count = skin.joint_count().min(joints.len());
127        let mut skin_mats: Vec<TransformMatrix> = Vec::with_capacity(joint_count);
128
129        for i in 0..joint_count {
130            let joint_world = match joints[i] {
131                Some(joint_cid) => TransformSystem::world_model(world, joint_cid)
132                    .unwrap_or_else(Self::mat4_identity),
133                None => Self::mat4_identity(),
134            };
135            let ibm = skin.inverse_bind_matrices[i];
136
137            let skin_mat = Self::mat4_mul(Self::mat4_mul(inv_mesh_world, joint_world), ibm);
138            skin_mats.push(skin_mat);
139        }
140
141        Some(skin_mats)
142    }
143
144    fn find_parent_renderable(world: &World, mut cid: ComponentId) -> Option<ComponentId> {
145        while let Some(parent) = world.parent_of(cid) {
146            if world
147                .get_component_by_id_as::<RenderableComponent>(parent)
148                .is_some()
149            {
150                return Some(parent);
151            }
152            cid = parent;
153        }
154        None
155    }
156
157    fn find_parent_transform(world: &World, mut cid: ComponentId) -> Option<ComponentId> {
158        while let Some(parent) = world.parent_of(cid) {
159            if world
160                .get_component_by_id_as::<TransformComponent>(parent)
161                .is_some()
162            {
163                return Some(parent);
164            }
165            cid = parent;
166        }
167        None
168    }
169
170    /// Resolve the GLTFComponent that owns the instance joints for this skin.
171    ///
172    /// Important: GLTFSystem spawns the node/renderable subtree under the nearest Transform
173    /// ancestor (the "anchor"), and the GLTFComponent itself is typically a *child* of that
174    /// anchor Transform. That means spawned nodes are often siblings (not descendants) of the
175    /// GLTFComponent.
176    fn find_nearest_gltf_component_for_skin(
177        &self,
178        world: &World,
179        mut cid: ComponentId,
180        skin_id: SkinId,
181    ) -> Option<ComponentId> {
182        let mut first_candidate: Option<ComponentId> = None;
183
184        loop {
185            // Candidate 1: the node itself.
186            if world.get_component_by_id_as::<GLTFComponent>(cid).is_some() {
187                if self.instance_joints.contains_key(&(cid, skin_id)) {
188                    return Some(cid);
189                }
190                if first_candidate.is_none() {
191                    first_candidate = Some(cid);
192                }
193            }
194
195            // Candidate 2: any GLTFComponent child of this node.
196            for &child in world.children_of(cid) {
197                if world
198                    .get_component_by_id_as::<GLTFComponent>(child)
199                    .is_some()
200                {
201                    if self.instance_joints.contains_key(&(child, skin_id)) {
202                        return Some(child);
203                    }
204                    if first_candidate.is_none() {
205                        first_candidate = Some(child);
206                    }
207                }
208            }
209
210            let parent = world.parent_of(cid);
211            match parent {
212                Some(p) => cid = p,
213                None => return first_candidate,
214            }
215        }
216    }
217
218    fn rebuild_indices(
219        &mut self,
220        world: &World,
221        skinned: &[ComponentId],
222    ) -> HashMap<BindingKey, Vec<ComponentId>> {
223        self.joint_to_bindings.clear();
224        self.mesh_transform_to_bindings.clear();
225
226        let mut binding_to_renderables: HashMap<BindingKey, Vec<ComponentId>> = HashMap::new();
227
228        for &skinned_cid in skinned {
229            let Some(sm) = world.get_component_by_id_as::<SkinnedMeshComponent>(skinned_cid) else {
230                continue;
231            };
232            let Some(skin_id) = sm.skin_id else {
233                continue;
234            };
235
236            let Some(renderable_cid) = Self::find_parent_renderable(world, skinned_cid) else {
237                continue;
238            };
239
240            let Some(gltf_component) =
241                self.find_nearest_gltf_component_for_skin(world, skinned_cid, skin_id)
242            else {
243                continue;
244            };
245
246            let Some(mesh_transform) = Self::find_parent_transform(world, renderable_cid) else {
247                continue;
248            };
249
250            let binding = BindingKey {
251                mesh_transform,
252                gltf_component,
253                skin_id,
254            };
255
256            binding_to_renderables
257                .entry(binding)
258                .or_default()
259                .push(renderable_cid);
260        }
261
262        // Build reverse index: joint -> bindings.
263        for (&binding, _) in binding_to_renderables.iter() {
264            self.mesh_transform_to_bindings
265                .entry(binding.mesh_transform)
266                .or_default()
267                .push(binding);
268
269            let Some(joints) = self
270                .instance_joints
271                .get(&(binding.gltf_component, binding.skin_id))
272            else {
273                continue;
274            };
275
276            for &joint in joints.iter().flatten() {
277                self.joint_to_bindings
278                    .entry(joint)
279                    .or_default()
280                    .push(binding);
281            }
282        }
283
284        binding_to_renderables
285    }
286
287    /// Notify the system that a transform subtree changed.
288    ///
289    /// This walks the subtree and marks any skins referencing affected joint transforms dirty.
290    pub fn transform_subtree_changed(&mut self, world: &World, root: ComponentId) {
291        // Fast path: if we haven't indexed anything yet, the next tick will compute new bindings.
292        if self.joint_to_bindings.is_empty() && self.mesh_transform_to_bindings.is_empty() {
293            return;
294        }
295
296        let mut stack: Vec<ComponentId> = vec![root];
297        while let Some(node) = stack.pop() {
298            if let Some(bindings) = self.joint_to_bindings.get(&node) {
299                for &binding in bindings {
300                    self.dirty_bindings.insert(binding);
301                }
302            }
303
304            if let Some(bindings) = self.mesh_transform_to_bindings.get(&node) {
305                for &binding in bindings {
306                    self.dirty_bindings.insert(binding);
307                }
308            }
309
310            for &child in world.children_of(node) {
311                stack.push(child);
312            }
313        }
314    }
315}
316
317impl System for SkinnedMeshSystem {
318    fn tick(
319        &mut self,
320        world: &mut World,
321        visuals: &mut VisualWorld,
322        _input: &InputState,
323        _dt_sec: f32,
324    ) {
325        let debug_skin_apply = std::env::var("CAT_DEBUG_SKIN_APPLY")
326            .ok()
327            .map(|s| {
328                let s = s.trim().to_ascii_lowercase();
329                s == "1" || s == "true" || s == "on" || s == "yes"
330            })
331            .unwrap_or(false);
332
333        static APPLY_LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
334
335        let skinned: Vec<ComponentId> = world
336            .all_components()
337            .filter(|&cid| {
338                world
339                    .get_component_by_id_as::<SkinnedMeshComponent>(cid)
340                    .is_some()
341            })
342            .collect();
343
344        let binding_to_renderables = self.rebuild_indices(&*world, &skinned);
345
346        // Mark newly discovered bindings dirty so they get an initial palette upload.
347        for &binding in binding_to_renderables.keys() {
348            if self.known_bindings.insert(binding) {
349                self.dirty_bindings.insert(binding);
350            }
351        }
352
353        // Only update bindings that are marked dirty.
354        if self.dirty_bindings.is_empty() {
355            return;
356        }
357
358        let dirty: Vec<BindingKey> = self.dirty_bindings.iter().copied().collect();
359        self.dirty_bindings.clear();
360
361        for binding in dirty {
362            let skin_mats = match self.update_binding(&*world, visuals, binding) {
363                Some(v) => v,
364                None => {
365                    if debug_skin_apply {
366                        let n = APPLY_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
367                        if n < 16 {
368                            let has_skin = visuals.skin(binding.skin_id).is_some();
369                            let has_joints = self
370                                .instance_joints
371                                .contains_key(&(binding.gltf_component, binding.skin_id));
372                            println!(
373                                "[SkinnedMeshSystem] binding skipped: reason=missing_data has_skin={} has_instance_joints={} gltf_component={:?} mesh_transform={:?}",
374                                has_skin,
375                                has_joints,
376                                binding.gltf_component,
377                                binding.mesh_transform,
378                            );
379                        }
380                    }
381                    // If prerequisite data isn't ready yet, retry next tick.
382                    self.dirty_bindings.insert(binding);
383                    continue;
384                }
385            };
386
387            let Some(renderables) = binding_to_renderables.get(&binding) else {
388                continue;
389            };
390
391            let mut missing_handle = false;
392            let mut applied = 0usize;
393            let mut failed_apply = 0usize;
394
395            for &renderable_cid in renderables {
396                let Some(renderable) =
397                    world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
398                else {
399                    continue;
400                };
401                let Some(handle) = renderable.get_handle() else {
402                    missing_handle = true;
403                    continue;
404                };
405                let did = visuals.set_skin_matrices(handle, &skin_mats);
406                if did {
407                    applied += 1;
408                } else {
409                    failed_apply += 1;
410                }
411            }
412
413            if debug_skin_apply {
414                // Log a few times per run so we can see the pipeline come online.
415                let n = APPLY_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
416                if n < 16 {
417                    println!(
418                        "[SkinnedMeshSystem] binding applied: skin_mats={} renderables={} applied={} failed_apply={} missing_handle={} gltf_component={:?} mesh_transform={:?}",
419                        skin_mats.len(),
420                        renderables.len(),
421                        applied,
422                        failed_apply,
423                        missing_handle,
424                        binding.gltf_component,
425                        binding.mesh_transform,
426                    );
427                }
428            }
429
430            // If renderable instances aren't flushed yet, their handles will be missing here.
431            // Keep the binding dirty so we retry next tick and get an initial palette upload.
432            if missing_handle {
433                self.dirty_bindings.insert(binding);
434            }
435        }
436    }
437}