Skip to main content

concinnity_engine/gfx/
shape_preview.rs

1// Live CharacterShape re-resolution. GraphicsSystem resolves every shape once
2// at init (`graphics_system/character_shape.rs`); an editor dragging a slider
3// needs the same resolution against the running world's SkeletonPose each
4// frame, without rebuilding the world. This is that narrow seam: what a mesh
5// exposes to a shape (its morph-target and joint names) and a re-seed of its
6// pose through an edited shape.
7
8use crate::components::{CharacterCapsule, CharacterRig, CharacterShape, SkeletonPose};
9use crate::ecs::asset_id::AssetId;
10use crate::ecs::{SkinnedMeshHandle, World};
11use crate::gfx::graphics_system::character_shape;
12
13/// Each skinned mesh's morph-target names, indexed by handle. Published by
14/// GraphicsSystem while it loads the SkinnedMesh resource table.
15#[derive(Debug, Default, Clone)]
16pub(crate) struct SkinnedMeshMorphNames(pub Vec<Vec<String>>);
17
18/// The names a shape targeting one mesh can reference.
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct ShapeTarget {
21    /// The mesh's morph targets, in target order.
22    pub morph_names: Vec<String>,
23    /// The mesh's skeleton joints, in joint order.
24    pub joint_names: Vec<String>,
25}
26
27/// The handle of the skinned mesh interned as `name_id`, if the running world
28/// loaded one under that name.
29pub fn mesh_handle(world: &World, name_id: AssetId) -> Option<SkinnedMeshHandle> {
30    world
31        .resource::<crate::gfx::skinned_mesh_map::SkinnedMeshNameIndex>()?
32        .0
33        .get(&name_id)
34        .copied()
35}
36
37/// What the mesh behind `handle` exposes to a shape. `None` until the mesh
38/// has a pose in the world.
39pub fn target(world: &World, handle: SkinnedMeshHandle) -> Option<ShapeTarget> {
40    let pose = world
41        .query::<SkeletonPose>()
42        .find(|p| p.mesh_id == handle)?;
43    let morph_names = world
44        .resource::<SkinnedMeshMorphNames>()
45        .and_then(|m| m.0.get(handle.index()))
46        .cloned()
47        .unwrap_or_default();
48    Some(ShapeTarget {
49        morph_names,
50        joint_names: pose
51            .skeleton
52            .joints()
53            .iter()
54            .map(|j| j.name.clone())
55            .collect(),
56    })
57}
58
59/// Re-seed every pose of `shape.target` through `shape`, and size the mesh's
60/// rig capsule from `capsule` (the authored dimensions) through the new
61/// proportions. Returns whether a pose was found. The rig's physics body is
62/// not resized here; the rebuild that follows a committed edit does that.
63pub fn apply(
64    world: &mut World,
65    shape: &CharacterShape,
66    capsule: Option<&CharacterCapsule>,
67) -> bool {
68    let Some(handle) = shape.target else {
69        return false;
70    };
71    let morph_names = world
72        .resource::<SkinnedMeshMorphNames>()
73        .and_then(|m| m.0.get(handle.index()))
74        .cloned()
75        .unwrap_or_default();
76    let mut dims = None;
77    let mut found = false;
78    for pose in world
79        .query_mut::<SkeletonPose>()
80        .filter(|p| p.mesh_id == handle)
81    {
82        let layers = character_shape::layers(shape, &pose.skeleton, &morph_names);
83        if let Some(c) = capsule {
84            dims = Some(character_shape::proportioned_capsule(
85                c,
86                &pose.skeleton,
87                &layers.proportions,
88            ));
89        }
90        pose.set_shape(layers.morph_base, layers.proportions);
91        found = true;
92    }
93    if let Some((half_height, radius)) = dims {
94        for rig in world
95            .query_mut::<CharacterRig>()
96            .filter(|r| r.target == handle)
97        {
98            rig.half_height = half_height.max(0.05);
99            rig.radius = radius.max(0.05);
100        }
101    }
102    found
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::components::{JointProportion, ShapeSlider};
109    use crate::gfx::skeleton::{Joint, JointPose, Skeleton};
110
111    fn chain() -> Skeleton {
112        let joint = |name: &str, parent: Option<usize>, y: f32| Joint {
113            name: name.to_string(),
114            parent,
115            bind: JointPose {
116                translation: [0.0, y, 0.0],
117                ..Default::default()
118            },
119        };
120        Skeleton::new(vec![joint("root", None, 0.0), joint("head", Some(0), 2.0)])
121    }
122
123    fn world_with_pose() -> World {
124        let mut world = World::new();
125        world.add_component(SkeletonPose::new(SkinnedMeshHandle(0), 0, chain()));
126        world.insert_resource(SkinnedMeshMorphNames(vec![vec![
127            "jaw+".to_string(),
128            "jaw-".to_string(),
129        ]]));
130        world
131    }
132
133    #[test]
134    fn target_lists_the_pose_joints_and_published_morph_names() {
135        let world = world_with_pose();
136        let t = target(&world, SkinnedMeshHandle(0)).expect("pose present");
137        assert_eq!(t.morph_names, ["jaw+", "jaw-"]);
138        assert_eq!(t.joint_names, ["root", "head"]);
139        assert!(target(&world, SkinnedMeshHandle(1)).is_none());
140    }
141
142    #[test]
143    fn apply_reseeds_the_pose_and_reports_a_missing_target() {
144        let mut world = world_with_pose();
145        let shape = CharacterShape {
146            target: Some(SkinnedMeshHandle(0)),
147            sliders: vec![ShapeSlider {
148                name: "jaw".into(),
149                value: -0.5,
150            }],
151            proportions: vec![JointProportion {
152                joint: "root".into(),
153                scale: 2.0,
154                length: 0.0,
155            }],
156            ..Default::default()
157        };
158        assert!(apply(&mut world, &shape, None));
159        let pose = world.query::<SkeletonPose>().next().unwrap();
160        assert_eq!(pose.morph_base, [0.0, 0.5]);
161        assert_eq!(pose.joint_matrices[0][0][0], 2.0);
162        assert!(pose.updated);
163        let none = CharacterShape::default();
164        assert!(!apply(&mut world, &none, None));
165    }
166}