use crate::components::{CharacterCapsule, CharacterRig, CharacterShape, SkeletonPose};
use crate::ecs::asset_id::AssetId;
use crate::ecs::{SkinnedMeshHandle, World};
use crate::gfx::graphics_system::character_shape;
#[derive(Debug, Default, Clone)]
pub(crate) struct SkinnedMeshMorphNames(pub Vec<Vec<String>>);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ShapeTarget {
pub morph_names: Vec<String>,
pub joint_names: Vec<String>,
}
pub fn mesh_handle(world: &World, name_id: AssetId) -> Option<SkinnedMeshHandle> {
world
.resource::<crate::gfx::skinned_mesh_map::SkinnedMeshNameIndex>()?
.0
.get(&name_id)
.copied()
}
pub fn target(world: &World, handle: SkinnedMeshHandle) -> Option<ShapeTarget> {
let pose = world
.query::<SkeletonPose>()
.find(|p| p.mesh_id == handle)?;
let morph_names = world
.resource::<SkinnedMeshMorphNames>()
.and_then(|m| m.0.get(handle.index()))
.cloned()
.unwrap_or_default();
Some(ShapeTarget {
morph_names,
joint_names: pose
.skeleton
.joints()
.iter()
.map(|j| j.name.clone())
.collect(),
})
}
pub fn apply(
world: &mut World,
shape: &CharacterShape,
capsule: Option<&CharacterCapsule>,
) -> bool {
let Some(handle) = shape.target else {
return false;
};
let morph_names = world
.resource::<SkinnedMeshMorphNames>()
.and_then(|m| m.0.get(handle.index()))
.cloned()
.unwrap_or_default();
let mut dims = None;
let mut found = false;
for pose in world
.query_mut::<SkeletonPose>()
.filter(|p| p.mesh_id == handle)
{
let layers = character_shape::layers(shape, &pose.skeleton, &morph_names);
if let Some(c) = capsule {
dims = Some(character_shape::proportioned_capsule(
c,
&pose.skeleton,
&layers.proportions,
));
}
pose.set_shape(layers.morph_base, layers.proportions);
found = true;
}
if let Some((half_height, radius)) = dims {
for rig in world
.query_mut::<CharacterRig>()
.filter(|r| r.target == handle)
{
rig.half_height = half_height.max(0.05);
rig.radius = radius.max(0.05);
}
}
found
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{JointProportion, ShapeSlider};
use crate::gfx::skeleton::{Joint, JointPose, Skeleton};
fn chain() -> Skeleton {
let joint = |name: &str, parent: Option<usize>, y: f32| Joint {
name: name.to_string(),
parent,
bind: JointPose {
translation: [0.0, y, 0.0],
..Default::default()
},
};
Skeleton::new(vec![joint("root", None, 0.0), joint("head", Some(0), 2.0)])
}
fn world_with_pose() -> World {
let mut world = World::new();
world.add_component(SkeletonPose::new(SkinnedMeshHandle(0), 0, chain()));
world.insert_resource(SkinnedMeshMorphNames(vec![vec![
"jaw+".to_string(),
"jaw-".to_string(),
]]));
world
}
#[test]
fn target_lists_the_pose_joints_and_published_morph_names() {
let world = world_with_pose();
let t = target(&world, SkinnedMeshHandle(0)).expect("pose present");
assert_eq!(t.morph_names, ["jaw+", "jaw-"]);
assert_eq!(t.joint_names, ["root", "head"]);
assert!(target(&world, SkinnedMeshHandle(1)).is_none());
}
#[test]
fn apply_reseeds_the_pose_and_reports_a_missing_target() {
let mut world = world_with_pose();
let shape = CharacterShape {
target: Some(SkinnedMeshHandle(0)),
sliders: vec![ShapeSlider {
name: "jaw".into(),
value: -0.5,
}],
proportions: vec![JointProportion {
joint: "root".into(),
scale: 2.0,
length: 0.0,
}],
..Default::default()
};
assert!(apply(&mut world, &shape, None));
let pose = world.query::<SkeletonPose>().next().unwrap();
assert_eq!(pose.morph_base, [0.0, 0.5]);
assert_eq!(pose.joint_matrices[0][0][0], 2.0);
assert!(pose.updated);
let none = CharacterShape::default();
assert!(!apply(&mut world, &none, None));
}
}