use crate::chunks::bone::M2Bone;
use crate::chunks::vertex::M2Vertex;
use crate::common::{C3Vector, Quaternion};
use glam::{Mat4, Quat, Vec3};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SkinningOptions {
pub normalize_weights: bool,
pub weight_threshold: f32,
pub validate_bone_indices: bool,
pub handle_invalid_indices: bool,
}
impl Default for SkinningOptions {
fn default() -> Self {
Self {
normalize_weights: true,
weight_threshold: 0.001,
validate_bone_indices: true,
handle_invalid_indices: true,
}
}
}
#[derive(Debug, Clone)]
pub struct BoneTransform {
pub matrix: Mat4,
pub local_matrix: Mat4,
pub inverse_bind_matrix: Mat4,
pub is_valid: bool,
}
impl Default for BoneTransform {
fn default() -> Self {
Self {
matrix: Mat4::IDENTITY,
local_matrix: Mat4::IDENTITY,
inverse_bind_matrix: Mat4::IDENTITY,
is_valid: true,
}
}
}
pub struct M2Skinner {
bone_transforms: Vec<BoneTransform>,
bone_hierarchy: HashMap<usize, usize>,
bone_count: usize,
options: SkinningOptions,
}
impl M2Skinner {
pub fn new(bones: &[M2Bone], options: SkinningOptions) -> Self {
let bone_count = bones.len();
let mut bone_transforms = vec![BoneTransform::default(); bone_count];
let mut bone_hierarchy = HashMap::new();
for (index, bone) in bones.iter().enumerate() {
if bone.parent_bone >= 0 && (bone.parent_bone as usize) < bone_count {
bone_hierarchy.insert(index, bone.parent_bone as usize);
}
}
for (index, bone) in bones.iter().enumerate() {
bone_transforms[index].inverse_bind_matrix =
Self::calculate_bind_matrix(&bone.pivot).inverse();
}
Self {
bone_transforms,
bone_hierarchy,
bone_count,
options,
}
}
pub fn calculate_bind_pose(&mut self) {
for i in 0..self.bone_count {
self.bone_transforms[i].local_matrix = Mat4::IDENTITY;
self.bone_transforms[i].matrix = Mat4::IDENTITY;
}
self.update_bone_hierarchy();
}
pub fn calculate_animated_pose(&mut self, bones: &[M2Bone], _animation_frame: f32) {
for (index, _bone) in bones.iter().enumerate() {
if index < self.bone_count {
let translation = Vec3::ZERO;
let rotation = Quat::IDENTITY;
let scale = Vec3::ONE;
let local_matrix =
Mat4::from_scale_rotation_translation(scale, rotation, translation);
self.bone_transforms[index].local_matrix = local_matrix;
}
}
self.update_bone_hierarchy();
}
fn update_bone_hierarchy(&mut self) {
for i in 0..self.bone_count {
if !self.bone_hierarchy.contains_key(&i) {
self.bone_transforms[i].matrix = self.bone_transforms[i].local_matrix;
}
}
let mut processed = vec![false; self.bone_count];
let mut changed = true;
while changed {
changed = false;
for i in 0..self.bone_count {
if processed[i] {
continue;
}
if let Some(&parent_index) = self.bone_hierarchy.get(&i) {
if processed[parent_index] {
let parent_matrix = self.bone_transforms[parent_index].matrix;
let local_matrix = self.bone_transforms[i].local_matrix;
self.bone_transforms[i].matrix = parent_matrix * local_matrix;
processed[i] = true;
changed = true;
}
} else {
processed[i] = true;
changed = true;
}
}
}
}
pub fn skin_vertices(&self, vertices: &[M2Vertex]) -> Vec<C3Vector> {
vertices
.iter()
.map(|vertex| self.skin_single_vertex(vertex))
.collect()
}
pub fn skin_single_vertex(&self, vertex: &M2Vertex) -> C3Vector {
let bind_position = vertex.position.to_glam();
let weights = self.normalize_bone_weights(&vertex.bone_weights);
let mut final_position = Vec3::ZERO;
let mut total_weight = 0.0f32;
for (i, &weight) in weights.iter().enumerate() {
if weight < self.options.weight_threshold {
continue;
}
let bone_index = vertex.bone_indices[i] as usize;
if bone_index >= self.bone_count {
if self.options.handle_invalid_indices {
let clamped_index = bone_index.min(self.bone_count - 1);
if let Some(transform) = self.bone_transforms.get(clamped_index) {
let skinning_matrix = transform.matrix * transform.inverse_bind_matrix;
let transformed = skinning_matrix.transform_point3(bind_position);
final_position += transformed * weight;
total_weight += weight;
}
}
continue;
}
let transform = &self.bone_transforms[bone_index];
if !transform.is_valid {
continue;
}
let skinning_matrix = transform.matrix * transform.inverse_bind_matrix;
let transformed = skinning_matrix.transform_point3(bind_position);
final_position += transformed * weight;
total_weight += weight;
}
if total_weight < self.options.weight_threshold {
return vertex.position;
}
if self.options.normalize_weights && total_weight > 0.0 {
final_position /= total_weight;
}
C3Vector::from_glam(final_position)
}
fn normalize_bone_weights(&self, weights: &[u8; 4]) -> [f32; 4] {
let mut normalized = [0.0f32; 4];
let mut total = 0.0f32;
for i in 0..4 {
normalized[i] = weights[i] as f32 / 255.0;
total += normalized[i];
}
if self.options.normalize_weights && total > self.options.weight_threshold {
for weight in &mut normalized {
*weight /= total;
}
} else if total <= self.options.weight_threshold {
normalized[0] = 1.0;
}
normalized
}
fn calculate_bind_matrix(pivot: &C3Vector) -> Mat4 {
Mat4::from_translation(pivot.to_glam())
}
pub fn get_bone_transform(&self, index: usize) -> Option<&BoneTransform> {
self.bone_transforms.get(index)
}
pub fn bone_count(&self) -> usize {
self.bone_count
}
pub fn set_options(&mut self, options: SkinningOptions) {
self.options = options;
}
pub fn options(&self) -> &SkinningOptions {
&self.options
}
}
impl M2Skinner {
pub fn quaternion_to_matrix(quat: &Quaternion) -> Mat4 {
Mat4::from_quat(quat.to_glam())
}
pub fn create_transform_matrix(
translation: &C3Vector,
rotation: &Quaternion,
scale: &C3Vector,
) -> Mat4 {
Mat4::from_scale_rotation_translation(
scale.to_glam(),
rotation.to_glam(),
translation.to_glam(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunks::bone::M2BoneFlags;
use crate::chunks::m2_track::{M2TrackQuat, M2TrackVec3};
fn create_test_bone(bone_id: i32, parent: i16, pivot: C3Vector) -> M2Bone {
M2Bone {
bone_id,
flags: M2BoneFlags::empty(),
parent_bone: parent,
submesh_id: 0,
unknown: [0, 0],
bone_name_crc: None,
translation: M2TrackVec3::new(),
rotation: M2TrackQuat::new(),
scale: M2TrackVec3::new(),
pivot,
}
}
fn create_test_vertex(pos: C3Vector, weights: [u8; 4], indices: [u8; 4]) -> M2Vertex {
M2Vertex {
position: pos,
bone_weights: weights,
bone_indices: indices,
normal: C3Vector {
x: 0.0,
y: 1.0,
z: 0.0,
},
tex_coords: crate::common::C2Vector { x: 0.0, y: 0.0 },
tex_coords2: None,
}
}
#[test]
fn test_skinner_creation() {
let bones = vec![
create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
),
create_test_bone(
1,
0,
C3Vector {
x: 1.0,
y: 0.0,
z: 0.0,
},
),
];
let skinner = M2Skinner::new(&bones, SkinningOptions::default());
assert_eq!(skinner.bone_count(), 2);
assert!(skinner.bone_hierarchy.contains_key(&1));
assert_eq!(skinner.bone_hierarchy[&1], 0);
}
#[test]
fn test_bind_pose_calculation() {
let bones = vec![create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
)];
let mut skinner = M2Skinner::new(&bones, SkinningOptions::default());
skinner.calculate_bind_pose();
let transform = skinner.get_bone_transform(0).unwrap();
assert!(transform.is_valid);
assert_eq!(transform.matrix, Mat4::IDENTITY);
}
#[test]
fn test_single_bone_skinning() {
let bones = vec![create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
)];
let mut skinner = M2Skinner::new(&bones, SkinningOptions::default());
skinner.calculate_bind_pose();
let vertex = create_test_vertex(
C3Vector {
x: 1.0,
y: 2.0,
z: 3.0,
},
[255, 0, 0, 0], [0, 0, 0, 0],
);
let result = skinner.skin_single_vertex(&vertex);
assert!((result.x - 1.0).abs() < 0.001);
assert!((result.y - 2.0).abs() < 0.001);
assert!((result.z - 3.0).abs() < 0.001);
}
#[test]
fn test_multi_bone_skinning() {
let bones = vec![
create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
),
create_test_bone(
1,
-1,
C3Vector {
x: 2.0,
y: 0.0,
z: 0.0,
},
),
];
let mut skinner = M2Skinner::new(&bones, SkinningOptions::default());
skinner.calculate_bind_pose();
let vertex = create_test_vertex(
C3Vector {
x: 1.0,
y: 0.0,
z: 0.0,
},
[128, 127, 0, 0], [0, 1, 0, 0],
);
let result = skinner.skin_single_vertex(&vertex);
assert!(result.x != vertex.position.x || result.y != vertex.position.y);
}
#[test]
fn test_weight_normalization() {
let weights = [100, 100, 55, 0];
let bones = vec![create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
)];
let skinner = M2Skinner::new(&bones, SkinningOptions::default());
let normalized = skinner.normalize_bone_weights(&weights);
let total: f32 = normalized.iter().sum();
assert!(
(total - 1.0).abs() < 0.001,
"Weights should sum to 1.0, got {}",
total
);
assert!(normalized[0] > 0.35 && normalized[0] < 0.45);
assert!(normalized[1] > 0.35 && normalized[1] < 0.45);
assert!(normalized[2] > 0.15 && normalized[2] < 0.25);
assert!(normalized[3] < 0.001);
}
#[test]
fn test_invalid_bone_indices() {
let bones = vec![create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
)];
let mut skinner = M2Skinner::new(&bones, SkinningOptions::default());
skinner.calculate_bind_pose();
let vertex = create_test_vertex(
C3Vector {
x: 1.0,
y: 2.0,
z: 3.0,
},
[255, 0, 0, 0],
[99, 0, 0, 0], );
let result = skinner.skin_single_vertex(&vertex);
assert!((result.x - vertex.position.x).abs() < 2.0);
assert!((result.y - vertex.position.y).abs() < 2.0);
assert!((result.z - vertex.position.z).abs() < 2.0);
}
#[test]
fn test_zero_weights() {
let bones = vec![create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
)];
let mut skinner = M2Skinner::new(&bones, SkinningOptions::default());
skinner.calculate_bind_pose();
let vertex = create_test_vertex(
C3Vector {
x: 1.0,
y: 2.0,
z: 3.0,
},
[0, 0, 0, 0], [0, 0, 0, 0],
);
let result = skinner.skin_single_vertex(&vertex);
assert_eq!(result.x, vertex.position.x);
assert_eq!(result.y, vertex.position.y);
assert_eq!(result.z, vertex.position.z);
}
#[test]
fn test_bone_hierarchy() {
let bones = vec![
create_test_bone(
0,
-1,
C3Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
), create_test_bone(
1,
0,
C3Vector {
x: 1.0,
y: 0.0,
z: 0.0,
},
), create_test_bone(
2,
1,
C3Vector {
x: 0.0,
y: 1.0,
z: 0.0,
},
), ];
let mut skinner = M2Skinner::new(&bones, SkinningOptions::default());
skinner.calculate_bind_pose();
assert_eq!(skinner.bone_hierarchy[&1], 0);
assert_eq!(skinner.bone_hierarchy[&2], 1);
assert!(!skinner.bone_hierarchy.contains_key(&0));
for i in 0..3 {
let transform = skinner.get_bone_transform(i).unwrap();
assert!(transform.is_valid);
}
}
}