concinnity_engine/gfx/
shape_preview.rs1use 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#[derive(Debug, Default, Clone)]
16pub(crate) struct SkinnedMeshMorphNames(pub Vec<Vec<String>>);
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct ShapeTarget {
21 pub morph_names: Vec<String>,
23 pub joint_names: Vec<String>,
25}
26
27pub 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
37pub 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
59pub 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}