//! Backend-independent avatar hierarchy and rigged-attachment math. #![allow( clippy::needless_pass_by_value, clippy::too_many_arguments, clippy::unnecessary_wraps )] // Mapped signatures and recursive hierarchy state are fixed. use crate::Error; use crate::animesh_skinning::MeshSkinData; use crate::rendering::{BoneTransform, Joint, LindenSkeleton}; use crate::visual_catalog::VisualParams; use libremetaverse_types::compat::{ Matrix4x4, Quaternion as NumericsQuaternion, Vector3 as NumericsVector3, }; use libremetaverse_types::{Matrix4, Quaternion, UUID, Vector3}; use roxmltree::Document; use std::collections::HashMap; use std::path::Path; const MAX_JOINTS: usize = 512; const MAX_LAD_BYTES: u64 = 8 * 1024 * 1024; const MAX_ATTACHMENT_POINTS: usize = 256; const MAX_MESH_DEFINITIONS: usize = 512; #[derive(Clone, Debug, PartialEq)] pub struct AvatarAttachmentPoint { group: i32, id: i32, joint: String, location: String, name: String, position: Vector3, rotation: Vector3, visible_in_first_person: bool, } impl AvatarAttachmentPoint { pub(crate) fn native_group(&self) -> i32 { self.group } pub(crate) fn native_id(&self) -> i32 { self.id } pub(crate) fn native_joint(&self) -> String { self.joint.clone() } pub(crate) fn native_location(&self) -> String { self.location.clone() } pub(crate) fn native_name(&self) -> String { self.name.clone() } pub(crate) fn native_position(&self) -> Vector3 { self.position } pub(crate) fn native_rotation(&self) -> Vector3 { self.rotation } pub(crate) fn native_visible_in_first_person(&self) -> bool { self.visible_in_first_person } } #[derive(Clone, Debug, PartialEq)] pub struct AvatarMeshDefinition { file_name: String, lod_level: i32, min_pixel_width: i32, type_: String, } impl AvatarMeshDefinition { pub(crate) fn native_file_name(&self) -> String { self.file_name.clone() } pub(crate) fn native_lod_level(&self) -> i32 { self.lod_level } pub(crate) fn native_min_pixel_width(&self) -> i32 { self.min_pixel_width } pub(crate) fn native_type(&self) -> String { self.type_.clone() } } #[derive(Clone, Debug, PartialEq)] pub struct LindenAvatarDefinition { attachment_points: Vec, mesh_definitions: Vec, skeleton: LindenSkeleton, } impl LindenAvatarDefinition { pub(crate) fn native_load( lad_file_name: Option, skeleton_file_name: Option, ) -> Result { let skeleton = LindenSkeleton::load_with_string(skeleton_file_name)?; let owned_xml; let xml = if let Some(file_name) = lad_file_name { owned_xml = read_bounded(&file_name)?; owned_xml.as_str() } else { include_str!("../assets/avatar_lad.xml") }; let document = Document::parse(xml).map_err(|_| Error::Argument)?; let mut attachment_points = Vec::new(); let mut mesh_definitions = Vec::new(); for node in document.descendants().filter(roxmltree::Node::is_element) { match node.tag_name().name() { "attachment_point" => { let Some(id) = parse_i32(node.attribute("id")) else { continue; }; if attachment_points.len() >= MAX_ATTACHMENT_POINTS { return Err(Error::Argument); } attachment_points.push(AvatarAttachmentPoint { group: parse_i32(node.attribute("group")).unwrap_or_default(), id, joint: node.attribute("joint").unwrap_or_default().to_owned(), location: node.attribute("location").unwrap_or_default().to_owned(), name: node.attribute("name").unwrap_or_default().to_owned(), position: parse_vector(node.attribute("position")), rotation: parse_vector(node.attribute("rotation")), visible_in_first_person: node .attribute("visible_in_first_person") .is_some_and(|value| value.eq_ignore_ascii_case("true")), }); } "mesh" => { let (Some(type_), Some(lod_level)) = (node.attribute("type"), parse_i32(node.attribute("lod"))) else { continue; }; if mesh_definitions.len() >= MAX_MESH_DEFINITIONS { return Err(Error::Argument); } mesh_definitions.push(AvatarMeshDefinition { file_name: node.attribute("file_name").unwrap_or_default().to_owned(), lod_level, min_pixel_width: parse_i32(node.attribute("min_pixel_width")) .unwrap_or_default(), type_: type_.to_owned(), }); } _ => {} } } Ok(Self { attachment_points, mesh_definitions, skeleton, }) } pub(crate) fn native_compute_bone_transforms( &self, param_values: &HashMap, ) -> Result, Error> { let mut result = HashMap::new(); let joints = self.skeleton.get_all_joints()?.collect::>(); if joints.len() > MAX_JOINTS { return Err(Error::Argument); } for joint in joints { seed_transform(&mut result, &joint.base); for volume in joint.collision_volume() { seed_transform(&mut result, &volume.base); } } for param in VisualParams::params().0.into_values() { let raw_value = param_values .get(¶m.param_id) .copied() .unwrap_or(param.default_value); let weight = raw_value.clamp(param.min_value, param.max_value); if !weight.is_finite() { return Err(Error::Argument); } for distortion in param.skeletal_distortions.unwrap_or_default() { let Some(transform) = result.get_mut(&distortion.bone_name) else { continue; }; transform.scale = add_scaled(transform.scale, distortion.scale_deformation, weight); if distortion.has_position_deformation { transform.position = add_scaled(transform.position, distortion.position_deformation, weight); } } for morph in param.volume_morphs.unwrap_or_default() { let Some(transform) = result.get_mut(&morph.bone_name) else { continue; }; if morph.has_scale { transform.scale = add_scaled(transform.scale, morph.scale_delta, weight); } if morph.has_position { transform.position = add_scaled(transform.position, morph.position_delta, weight); } } } if result .values() .any(|value| !finite(value.position) || !finite(value.scale)) { return Err(Error::Argument); } Ok(result) } pub(crate) fn native_get_attachment_point_by_id( &self, id: i32, ) -> Option { self.attachment_points .iter() .find(|point| point.id == id) .cloned() } pub(crate) fn native_get_attachment_point_by_name( &self, name: &str, ) -> Option { self.attachment_points .iter() .find(|point| point.name.eq_ignore_ascii_case(name)) .cloned() } pub(crate) fn native_attachment_points(&self) -> Vec { self.attachment_points.clone() } pub(crate) fn native_mesh_definitions(&self) -> Vec { self.mesh_definitions.clone() } pub(crate) fn native_skeleton(&self) -> LindenSkeleton { self.skeleton.clone() } } #[derive(Clone, Debug, Default)] pub struct AttachmentRiggedSkin { pub inv_bind_matrices: Vec, pub joint_names: Vec, pub joint_position_overrides: Vec<(String, NumericsVector3)>, pub joints: Vec, pub lock_scale_if_joint_position: bool, pub mesh_id: UUID, pub weights: Vec, } impl AttachmentRiggedSkin { pub(crate) fn native_new() -> Result { Ok(Self::default()) } } pub struct AvatarBoneMath; impl AvatarBoneMath { pub(crate) fn native_build_bone_world_matrices( skeleton: LindenSkeleton, bone_transforms: HashMap, ) -> Result, Error> { let mut output = HashMap::new(); let mut visited = 0; build_joint( &skeleton.bone(), Quaternion::identity(), Vector3::zero(), Vector3::one(), &bone_transforms, None, &mut output, &mut visited, )?; Ok(output) } pub(crate) fn native_compute_animated_bone_world_matrices( avatar_definition: &LindenAvatarDefinition, bone_transforms: &HashMap, rotation_deltas: &HashMap, ) -> Result, Error> { let mut output = HashMap::new(); let mut visited = 0; build_joint( &avatar_definition.skeleton.bone(), Quaternion::identity(), Vector3::zero(), Vector3::one(), bone_transforms, Some(rotation_deltas), &mut output, &mut visited, )?; Ok(output) } pub(crate) fn native_compute_attachment_bone_world_matrices( avatar_definition: &LindenAvatarDefinition, bone_transforms: &HashMap, rotation_deltas: &HashMap, ) -> Result, Error> { Self::native_compute_animated_bone_world_matrices( avatar_definition, bone_transforms, rotation_deltas, ) } pub(crate) fn native_strip_scale(matrix: Matrix4x4) -> Matrix4x4 { let mut value = matrix.0; for row in 0..3 { let offset = row * 4; let length = (value[offset].mul_add( value[offset], value[offset + 1].mul_add(value[offset + 1], value[offset + 2] * value[offset + 2]), )) .sqrt(); if length > 1.0e-5 { value[offset] /= length; value[offset + 1] /= length; value[offset + 2] /= length; } value[offset + 3] = 0.0; } Matrix4x4(value) } pub(crate) fn native_compute_ground_adjustment( transforms: Option<&HashMap>, ) -> f32 { let Some(transforms) = transforms else { return 1.0; }; let names = [ "mPelvis", "mSkull", "mNeck", "mChest", "mHead", "mTorso", "mHipLeft", "mKneeLeft", "mAnkleLeft", "mFootLeft", ]; let Some(values) = names .iter() .map(|name| transforms.get(*name)) .collect::>>() else { return 1.0; }; let [ pelvis, skull, neck, chest, head, torso, hip, knee, ankle, foot, ] = values.as_slice() else { return 1.0; }; let pelvis_to_foot = hip.position.z * pelvis.scale.z - knee.position.z * hip.scale.z - ankle.position.z * knee.scale.z - foot.position.z * ankle.scale.z; let height = pelvis_to_foot + std::f32::consts::SQRT_2 * skull.position.z * head.scale.z + head.position.z * neck.scale.z + neck.position.z * chest.scale.z + chest.position.z * torso.scale.z + torso.position.z * pelvis.scale.z; let adjustment = height - pelvis_to_foot; if adjustment.is_finite() && adjustment > 0.0 { adjustment } else { 1.0 } } } fn build_joint( joint: &Joint, parent_rotation: Quaternion, parent_position: Vector3, parent_scale: Vector3, transforms: &HashMap, rotation_deltas: Option<&HashMap>, output: &mut HashMap, visited: &mut usize, ) -> Result<(), Error> { *visited = visited.checked_add(1).ok_or(Error::Argument)?; if *visited > MAX_JOINTS { return Err(Error::Argument); } let name = joint.base.name(); let transform = transforms.get(&name); let position = transform.map_or_else( || vector(&joint.base.pos(), Vector3::zero()), |v| v.position, ); let scale = transform.map_or_else(|| vector(&joint.base.scale(), Vector3::one()), |v| v.scale); if !finite(position) || !finite(scale) { return Err(Error::Argument); } let mut rotation = euler(&joint.base.rot())?; if let Some(delta) = rotation_deltas.and_then(|values| values.get(&name)) { rotation = Quaternion::multiply_with_quaternion_quaternion(rotation, quaternion(*delta)?)?; } let world_rotation = Quaternion::multiply_with_quaternion_quaternion(parent_rotation, rotation)?; let scaled_position = Vector3::multiply_with_vector3_vector3(position, parent_scale)?; let rotated_position = Vector3::transform_normal( scaled_position, Matrix4::create_from_quaternion(parent_rotation)?, )?; let world_position = Vector3::add_with_vector3_vector3(rotated_position, parent_position)?; let world = Matrix4::multiply_with_matrix4_matrix4( Matrix4::multiply_with_matrix4_matrix4( Matrix4::create_scale(scale)?, Matrix4::create_from_quaternion(world_rotation)?, )?, Matrix4::create_translation(world_position)?, )?; let world = numerics(world); if !name.is_empty() { output.insert(name, world); } for alias in joint.get_aliases_list()? { output.entry(alias).or_insert(world); } for volume in joint.collision_volume() { *visited = visited.checked_add(1).ok_or(Error::Argument)?; if *visited > MAX_JOINTS { return Err(Error::Argument); } let volume_name = volume.base.name(); let volume_transform = transforms.get(&volume_name); let volume_position = volume_transform.map_or_else( || vector(&volume.base.pos(), Vector3::zero()), |value| value.position, ); let volume_scale = volume_transform.map_or_else( || vector(&volume.base.scale(), Vector3::one()), |value| value.scale, ); if !finite(volume_position) || !finite(volume_scale) { return Err(Error::Argument); } let volume_rotation = euler(&volume.base.rot())?; let volume_world_rotation = Quaternion::multiply_with_quaternion_quaternion(world_rotation, volume_rotation)?; let volume_scaled_position = Vector3::multiply_with_vector3_vector3(volume_position, scale)?; let volume_rotated_position = Vector3::transform_normal( volume_scaled_position, Matrix4::create_from_quaternion(world_rotation)?, )?; let volume_world_position = Vector3::add_with_vector3_vector3(volume_rotated_position, world_position)?; let volume_world = Matrix4::multiply_with_matrix4_matrix4( Matrix4::multiply_with_matrix4_matrix4( Matrix4::create_scale(volume_scale)?, Matrix4::create_from_quaternion(volume_world_rotation)?, )?, Matrix4::create_translation(volume_world_position)?, )?; if !volume_name.is_empty() { output.entry(volume_name).or_insert(numerics(volume_world)); } } for child in joint.bone().unwrap_or_default() { build_joint( &child, world_rotation, world_position, scale, transforms, rotation_deltas, output, visited, )?; } Ok(()) } fn vector(values: &[f32], fallback: Vector3) -> Vector3 { if values.len() < 3 { fallback } else { Vector3 { x: values[0], y: values[1], z: values[2], } } } fn finite(value: Vector3) -> bool { value.x.is_finite() && value.y.is_finite() && value.z.is_finite() } fn read_bounded(path: impl AsRef) -> Result { let path = path.as_ref(); if std::fs::metadata(path).map_err(|_| Error::Argument)?.len() > MAX_LAD_BYTES { return Err(Error::Argument); } let contents = std::fs::read_to_string(path).map_err(|_| Error::Argument)?; if contents.len() as u64 > MAX_LAD_BYTES { return Err(Error::Argument); } Ok(contents) } fn parse_i32(value: Option<&str>) -> Option { value?.parse().ok() } fn parse_vector(value: Option<&str>) -> Vector3 { let mut values = value.unwrap_or_default().split_ascii_whitespace(); let mut next = || { values .next() .and_then(|part| part.parse::().ok()) .unwrap_or_default() }; Vector3 { x: next(), y: next(), z: next(), } } fn seed_transform( output: &mut HashMap, joint: &crate::rendering::JointBase, ) { let name = joint.name(); if name.is_empty() { return; } output.insert( name, BoneTransform { position: vector(&joint.pos(), Vector3::zero()), scale: vector(&joint.scale(), Vector3::one()), }, ); } fn add_scaled(value: Vector3, delta: Vector3, weight: f32) -> Vector3 { Vector3 { x: delta.x.mul_add(weight, value.x), y: delta.y.mul_add(weight, value.y), z: delta.z.mul_add(weight, value.z), } } fn quaternion(value: NumericsQuaternion) -> Result { if value.0.iter().all(|component| component.is_finite()) { Quaternion::new_with_single_single_single_single( value.0[0], value.0[1], value.0[2], value.0[3], ) } else { Err(Error::Argument) } } fn euler(values: &[f32]) -> Result { let value = vector(values, Vector3::zero()); Quaternion::create_from_eulers_with_single_single_single( value.x.to_radians(), value.y.to_radians(), value.z.to_radians(), ) } fn numerics(value: Matrix4) -> Matrix4x4 { Matrix4x4([ value.m11, value.m12, value.m13, value.m14, value.m21, value.m22, value.m23, value.m24, value.m31, value.m32, value.m33, value.m34, value.m41, value.m42, value.m43, value.m44, ]) } pub struct RiggedSkinMath; impl RiggedSkinMath { pub(crate) fn native_floats_to_matrix(values: &[f32]) -> Matrix4x4 { let Ok(values) = <&[f32; 16]>::try_from(values) else { return Matrix4x4::default(); }; Matrix4x4(*values) } pub(crate) fn native_build_inv_bind_matrices( skin: &MeshSkinData, ) -> Result, Error> { if skin.joint_names.len() > MAX_JOINTS { return Err(Error::Argument); } Ok((0..skin.joint_names.len()) .map(|index| { let start = index * 16; skin.inverse_bind_matrices .get(start..start + 16) .map_or_else(identity, Self::native_floats_to_matrix) }) .collect()) } pub(crate) fn native_extract_joint_position_overrides( skin: &MeshSkinData, ) -> Result, Error> { let count = skin.joint_names.len(); if count > MAX_JOINTS { return Err(Error::Argument); } if skin.alt_inverse_bind_matrices.len() / 16 != count || !skin.alt_inverse_bind_matrices.len().is_multiple_of(16) { return Ok(Vec::new()); } Ok(skin .joint_names .iter() .enumerate() .filter(|(_, name)| !name.is_empty()) .map(|(index, name)| { let offset = index * 16; ( name.clone(), NumericsVector3([ skin.alt_inverse_bind_matrices[offset + 12], skin.alt_inverse_bind_matrices[offset + 13], skin.alt_inverse_bind_matrices[offset + 14], ]), ) }) .collect()) } pub(crate) fn native_normalize_skin_weights( joint_count: i32, joints: [&mut i32; 4], weights: [&mut f32; 4], ) { let mut sum = 0.0; for index in 0..4 { if *joints[index] < 0 || *joints[index] >= joint_count { *weights[index] = 0.0; } *weights[index] = if weights[index].is_finite() { weights[index].clamp(0.0, 1.0) } else { 0.0 }; sum += *weights[index]; } if sum > 1.0e-6 { for weight in weights { *weight /= sum; } } else { for index in 0..4 { *joints[index] = 0; *weights[index] = if index == 0 && joint_count > 0 { 1.0 } else { 0.0 }; } } } } fn identity() -> Matrix4x4 { Matrix4x4([ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]) } #[cfg(test)] mod tests { use super::*; #[test] fn embedded_avatar_definition_exposes_generated_skeleton_and_lad_records() { let definition = LindenAvatarDefinition::native_load(None, None).expect("avatar definition"); assert!(!definition.native_attachment_points().is_empty()); assert!(!definition.native_mesh_definitions().is_empty()); assert!( definition .native_skeleton() .get_all_joints() .expect("joints") .count() > 1 ); let point = definition.native_attachment_points()[0].clone(); assert_eq!( definition .native_get_attachment_point_by_id(point.native_id()) .expect("point by id"), point ); assert_eq!( definition .native_get_attachment_point_by_name(&point.native_name().to_uppercase()) .expect("point by name"), point ); } #[test] fn animated_and_attachment_world_matrices_apply_rotation_deltas() { let definition = LindenAvatarDefinition::native_load(None, None).expect("avatar definition"); let transforms = definition .native_compute_bone_transforms(&HashMap::new()) .expect("bone transforms"); let bind = AvatarBoneMath::native_compute_animated_bone_world_matrices( &definition, &transforms, &HashMap::new(), ) .expect("bind matrices"); let delta = NumericsQuaternion([ 0.0, 0.0, std::f32::consts::FRAC_1_SQRT_2, std::f32::consts::FRAC_1_SQRT_2, ]); let deltas = HashMap::from([("mPelvis".to_owned(), delta)]); let animated = AvatarBoneMath::native_compute_animated_bone_world_matrices( &definition, &transforms, &deltas, ) .expect("animated matrices"); let attachment = AvatarBoneMath::native_compute_attachment_bone_world_matrices( &definition, &transforms, &deltas, ) .expect("attachment matrices"); assert_ne!(animated["mPelvis"], bind["mPelvis"]); assert_eq!(attachment, animated); } }