use serde::{Deserialize, Serialize};
use crate::{
custom_index::NameMap,
data::runtime::{skeleton::BoneIdx, slot::SlotIdx},
new_index,
};
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MeshMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slot: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skin: Option<String>,
}
new_index!(MeshIdx);
#[derive(Debug, Clone)]
pub struct MeshData {
pub meta: MeshMetadata,
pub texture_name: String,
pub uvs: Vec<f32>,
pub vertex_count: usize,
pub triangles: Vec<u16>,
pub bones: Option<Vec<BoneIdx>>,
pub influence_counts: Vec<u8>,
pub vertices_locals: Vec<f32>,
pub influence_bone_indices: Vec<u8>,
pub influence_weights: Vec<f32>,
pub clip_end_slot: Option<SlotIdx>,
}
#[derive(Debug, Clone)]
pub struct MeshSet {
pub data_map: NameMap<MeshIdx, MeshData>,
}
impl Default for MeshSet {
fn default() -> Self {
Self::new()
}
}
impl MeshSet {
pub fn new() -> Self {
Self {
data_map: NameMap::new(),
}
}
pub fn gen_name_skin_slot_name(&self, skin: &str, slot: &str, name: &str) -> String {
format!("{}_{}_{}", skin, slot, name)
}
pub fn insert_mesh(&mut self, full_name: String, mesh_data: MeshData) -> MeshIdx {
self.data_map.insert(full_name, mesh_data)
}
pub fn get_index(&self, name: &str) -> Option<MeshIdx> {
self.data_map.get_index(name)
}
}
impl MeshData {
pub fn deform(
&self,
pose: &mut crate::data::runtime::skeleton::Pose,
skeleton: &crate::data::runtime::skeleton::Skeleton,
slot_bone: BoneIdx,
) -> Vec<glam::Vec2> {
deform_vertices(
self.vertex_count,
&self.bones,
&self.influence_counts,
&self.influence_bone_indices,
&self.influence_weights,
&self.vertices_locals,
None,
pose,
skeleton,
slot_bone,
)
}
pub fn deform_with_deform(
&self,
deform: &[f32],
pose: &mut crate::data::runtime::skeleton::Pose,
skeleton: &crate::data::runtime::skeleton::Skeleton,
slot_bone: BoneIdx,
) -> Vec<glam::Vec2> {
deform_vertices(
self.vertex_count,
&self.bones,
&self.influence_counts,
&self.influence_bone_indices,
&self.influence_weights,
&self.vertices_locals,
Some(deform),
pose,
skeleton,
slot_bone,
)
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn deform_vertices(
vertex_count: usize,
bones: &Option<Vec<BoneIdx>>,
influence_counts: &[u8],
influence_bone_indices: &[u8],
influence_weights: &[f32],
vertices_locals: &[f32],
deform: Option<&[f32]>,
pose: &mut crate::data::runtime::skeleton::Pose,
skeleton: &crate::data::runtime::skeleton::Skeleton,
slot_bone: BoneIdx,
) -> Vec<glam::Vec2> {
let weighted = bones.is_some();
let world_affines: Vec<glam::Affine2> = match bones {
Some(bones) => bones
.iter()
.map(|&b| pose.get_world_affine(skeleton, b))
.collect(),
None => vec![pose.get_world_affine(skeleton, slot_bone)],
};
let mut positions = Vec::with_capacity(vertex_count);
let mut offset = 0usize;
for v in 0..vertex_count {
let count = influence_counts[v] as usize;
let mut p = glam::Vec2::ZERO;
for _ in 0..count {
let local_bone = influence_bone_indices[offset] as usize;
let weight = influence_weights[offset];
let (bx, by) = if weighted {
match deform {
Some(d) if d.len() >= 2 * (offset + 1) => (
vertices_locals[2 * offset] + d[2 * offset],
vertices_locals[2 * offset + 1] + d[2 * offset + 1],
),
_ => (
vertices_locals[2 * offset],
vertices_locals[2 * offset + 1],
),
}
} else {
match deform {
Some(d) if d.len() >= 2 * (v + 1) => (d[2 * v], d[2 * v + 1]),
_ => (vertices_locals[2 * v], vertices_locals[2 * v + 1]),
}
};
let local = glam::Vec2::new(bx, by);
let world = world_affines[local_bone];
p += world.transform_point2(local) * weight;
offset += 1;
}
positions.push(p);
}
positions
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cacl::affine::build_affine,
custom_index::AsIndex,
data::runtime::skeleton::{BoneDataStatic, Skeleton},
utils::default,
};
fn build_skeleton() -> Skeleton {
let mut skeleton = Skeleton::new();
let root = skeleton.insert_bone("root", default()).unwrap();
let arm = BoneDataStatic {
translate_x: 50.0,
rotation_deg: 90.0,
parent_index: Some(root),
..Default::default()
};
skeleton.insert_bone("arm", arm).unwrap();
skeleton.build_children_map();
skeleton.build_roots();
skeleton
}
fn build_mesh() -> MeshData {
MeshData {
meta: Default::default(),
texture_name: "test".to_string(),
uvs: vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
vertex_count: 4,
triangles: vec![0, 1, 2, 0, 2, 3],
bones: Some(vec![unsafe { BoneIdx::from_index(0) }, unsafe {
BoneIdx::from_index(1)
}]),
influence_counts: vec![2, 2, 1, 1],
vertices_locals: vec![
0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0,
],
influence_bone_indices: vec![0, 1, 0, 1, 0, 0],
influence_weights: vec![0.3, 0.7, 0.3, 0.7, 1.0, 1.0],
clip_end_slot: None,
}
}
#[test]
fn deform_single_bone_identity() {
let skeleton = build_skeleton();
let mut pose = skeleton.default_pose();
let mesh = build_mesh();
let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(0) });
assert!((positions[2].x - 10.0).abs() < 1e-5);
assert!((positions[2].y - 10.0).abs() < 1e-5);
assert!((positions[3].x - 0.0).abs() < 1e-5);
assert!((positions[3].y - 10.0).abs() < 1e-5);
}
#[test]
fn deform_translated_bone() {
let skeleton = build_skeleton();
let mut pose = skeleton.default_pose();
let mesh = build_mesh();
pose.bone_data[unsafe { BoneIdx::from_index(0) }].translate_x = 100.0;
let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(0) });
for p in &positions {
assert!(p.x >= 99.9, "x={} should follow root translation", p.x);
}
assert!((positions[2].x - 110.0).abs() < 1e-4);
assert!((positions[2].y - 10.0).abs() < 1e-4);
}
#[test]
fn deform_weighted_mix() {
let skeleton = build_skeleton();
let mut pose = skeleton.default_pose();
let mesh = build_mesh();
let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(0) });
let v1 = positions[1];
assert!((v1.x - 38.0).abs() < 1e-4, "x={}", v1.x);
assert!((v1.y - 7.0).abs() < 1e-4, "y={}", v1.y);
let v0 = positions[0];
assert!((v0.x - 35.0).abs() < 1e-4);
assert!((v0.y - 0.0).abs() < 1e-4);
}
#[test]
fn deform_without_bones_uses_slot_bone() {
let skeleton = build_skeleton();
let mut pose = skeleton.default_pose();
let mut mesh = build_mesh();
mesh.bones = None;
mesh.influence_counts = vec![1, 1, 1, 1];
mesh.influence_bone_indices = vec![0, 0, 0, 0];
mesh.vertices_locals = vec![
0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0,
];
mesh.influence_weights = vec![1.0, 1.0, 1.0, 1.0];
let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(1) });
let world = build_affine(
[50.0, 0.0],
90.0,
[1.0, 1.0],
[0.0, 0.0],
crate::data::runtime::skeleton::ShearMode::KeepLength,
);
for (i, p) in positions.iter().enumerate() {
let local =
glam::Vec2::new(mesh.vertices_locals[2 * i], mesh.vertices_locals[2 * i + 1]);
assert!((p.x - world.transform_point2(local).x).abs() < 1e-4);
assert!((p.y - world.transform_point2(local).y).abs() < 1e-4);
}
}
#[test]
fn deform_weighted_with_delta() {
let skeleton = build_skeleton();
let mut pose = skeleton.default_pose();
let mesh = build_mesh();
let deform = vec![
0.0, 0.0, 0.0, 0.0, 5.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, ];
let positions = mesh.deform_with_deform(&deform, &mut pose, &skeleton, unsafe {
BoneIdx::from_index(0)
});
let v1 = positions[1];
assert!((v1.x - 39.5).abs() < 1e-4, "x={}", v1.x);
assert!((v1.y - 10.5).abs() < 1e-4, "y={}", v1.y);
assert!((positions[2].x - 10.0).abs() < 1e-4);
}
#[test]
fn deform_unweighted_absolute_positions() {
let skeleton = build_skeleton();
let mut pose = skeleton.default_pose();
let mut mesh = build_mesh();
mesh.bones = None;
mesh.influence_counts = vec![1, 1, 1, 1];
mesh.influence_bone_indices = vec![0, 0, 0, 0];
mesh.vertices_locals = vec![
0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0,
];
mesh.influence_weights = vec![1.0, 1.0, 1.0, 1.0];
let deform = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let positions = mesh.deform_with_deform(&deform, &mut pose, &skeleton, unsafe {
BoneIdx::from_index(0)
});
assert!((positions[0].x - 1.0).abs() < 1e-4);
assert!((positions[0].y - 2.0).abs() < 1e-4);
assert!((positions[1].x - 3.0).abs() < 1e-4);
assert!((positions[1].y - 4.0).abs() < 1e-4);
}
}