#![allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::float_cmp, clippy::too_many_lines )] // Fixture quantization is explicitly clamped to the u16 domain. use std::collections::{BTreeMap, HashMap}; use std::io::Write as _; use flate2::{Compression, write::ZlibEncoder}; use libremetaverse::import_export::{ModelFace, ModelPrim}; use libremetaverse::rendering::{DetailLevel, RiggedSkinMath, SimpleMesh, Vertex}; use libremetaverse::{Primitive, PrimitiveSculptData, PrimitiveTextureEntry}; use libremetaverse_rendering_mesh_foundry::{MeshFaceAux, MeshFoundry}; use libremetaverse_structured_data::{OSD, OSDParser}; use libremetaverse_types::{Error, SculptType, UUID, Vector2, Vector3}; fn compress(value: OSD) -> Vec { let bytes = OSDParser::serialize_llsd_binary_with_osd_boolean(value, false) .expect("serialize fixture section"); let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); encoder.write_all(&bytes).expect("compress fixture section"); encoder.finish().expect("finish fixture section") } fn pack(sections: BTreeMap<&str, OSD>) -> Vec { let mut payload = Vec::new(); let mut header = HashMap::new(); for (name, value) in sections { let section = compress(value); header.insert( name.to_owned(), OSD::Map(HashMap::from([ ( "offset".to_owned(), OSD::Integer(i32::try_from(payload.len()).expect("fixture offset")), ), ( "size".to_owned(), OSD::Integer(i32::try_from(section.len()).expect("fixture size")), ), ])), ); payload.extend(section); } header.insert("version".to_owned(), OSD::Integer(1)); let mut asset = OSDParser::serialize_llsd_binary_with_osd_boolean(OSD::Map(header), false) .expect("serialize fixture header"); asset.extend(payload); asset } fn quantized3(values: &[[f32; 3]], min: [f32; 3], max: [f32; 3]) -> Vec { values .iter() .flat_map(|value| { (0..3).flat_map(|axis| quantize(value[axis], min[axis], max[axis]).to_le_bytes()) }) .collect() } fn quantized2(values: &[[f32; 2]], min: [f32; 2], max: [f32; 2]) -> Vec { values .iter() .flat_map(|value| { (0..2).flat_map(|axis| quantize(value[axis], min[axis], max[axis]).to_le_bytes()) }) .collect() } fn quantize(value: f32, min: f32, max: f32) -> u16 { (((value - min) / (max - min)).clamp(0.0, 1.0) * 65_535.0).round() as u16 } fn domain2(min: Vector2, max: Vector2) -> OSD { OSD::Map(HashMap::from([ ( "Min".to_owned(), OSD::from_vector2(min).expect("domain min"), ), ( "Max".to_owned(), OSD::from_vector2(max).expect("domain max"), ), ])) } fn domain3(min: Vector3, max: Vector3) -> OSD { OSD::Map(HashMap::from([ ( "Min".to_owned(), OSD::from_vector3(min).expect("domain min"), ), ( "Max".to_owned(), OSD::from_vector3(max).expect("domain max"), ), ])) } fn identity() -> OSD { OSD::Array( [ 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, ] .into_iter() .map(OSD::Real) .collect(), ) } fn rigged_submesh(triangle: [u16; 3], weights: Vec) -> OSD { let positions = [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [-0.5, 0.5, 0.0]]; let normals = [[0.0, 0.0, 1.0]; 3]; let uv0 = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]; let uv1 = [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75]]; let tangents = [[1.0, 0.0, 0.0, -1.0]; 3]; let tangent_bytes: Vec = tangents .iter() .flat_map(|value| { value .iter() .flat_map(|component| quantize(*component, -1.0, 1.0).to_le_bytes()) }) .collect(); let triangle_bytes: Vec = triangle.into_iter().flat_map(u16::to_le_bytes).collect(); let mut map = HashMap::from([ ( "PositionDomain".to_owned(), domain3( Vector3 { x: -0.5, y: -0.5, z: -0.5, }, Vector3 { x: 0.5, y: 0.5, z: 0.5, }, ), ), ( "Position".to_owned(), OSD::Binary(quantized3(&positions, [-0.5; 3], [0.5; 3])), ), ( "Normal".to_owned(), OSD::Binary(quantized3(&normals, [-1.0; 3], [1.0; 3])), ), ( "TexCoord0Domain".to_owned(), domain2(Vector2::zero(), Vector2 { x: 1.0, y: 1.0 }), ), ( "TexCoord0".to_owned(), OSD::Binary(quantized2(&uv0, [0.0; 2], [1.0; 2])), ), ( "TexCoord1Domain".to_owned(), domain2(Vector2::zero(), Vector2 { x: 1.0, y: 1.0 }), ), ( "TexCoord1".to_owned(), OSD::Binary(quantized2(&uv1, [0.0; 2], [1.0; 2])), ), ("Tangent".to_owned(), OSD::Binary(tangent_bytes)), ("TriangleList".to_owned(), OSD::Binary(triangle_bytes)), ( "NormalizedScale".to_owned(), OSD::from_vector3(Vector3 { x: 2.0, y: 3.0, z: 4.0, }) .expect("normalized scale"), ), ]); if !weights.is_empty() { map.insert("Weights".to_owned(), OSD::Binary(weights)); } OSD::Map(map) } fn skin() -> OSD { OSD::Map(HashMap::from([ ( "joint_names".to_owned(), OSD::Array( [ OSD::String("mPelvis".to_owned()), OSD::String("mTorso".to_owned()), ] .into(), ), ), ( "inverse_bind_matrix".to_owned(), OSD::Array(vec![identity(), identity()]), ), ( "alt_inverse_bind_matrix".to_owned(), OSD::Array(vec![identity(), identity()]), ), ("bind_shape_matrix".to_owned(), identity()), ("pelvis_offset".to_owned(), OSD::Real(0.25)), ( "lock_scale_if_joint_position".to_owned(), OSD::Boolean(true), ), ])) } fn weights() -> Vec { let quarter = (0.25_f32 * 65_535.0).round() as u16; let three_quarters = (0.75_f32 * 65_535.0).round() as u16; let half = (0.5_f32 * 65_535.0).round() as u16; let mut bytes = vec![0]; bytes.extend(quarter.to_le_bytes()); bytes.push(1); bytes.extend(three_quarters.to_le_bytes()); bytes.push(0xff); bytes.push(0); bytes.extend(half.to_le_bytes()); bytes.push(1); bytes.extend(half.to_le_bytes()); bytes.push(0xff); bytes.push(0xff); bytes } fn rigged_asset(triangle: [u16; 3], weight_bytes: Vec) -> Vec { pack(BTreeMap::from([ ( "high_lod", OSD::Array(vec![rigged_submesh(triangle, weight_bytes)]), ), ("skin", skin()), ])) } fn mesh_hash(mesh: &SimpleMesh) -> u64 { let mut hash = 0xcbf2_9ce4_8422_2325_u64; for vertex in &mesh.vertices { for value in [ vertex.position.x, vertex.position.y, vertex.position.z, vertex.normal.x, vertex.normal.y, vertex.normal.z, vertex.tex_coord.x, vertex.tex_coord.y, ] { hash ^= u64::from(value.to_bits()); hash = hash.wrapping_mul(0x0000_0100_0000_01b3); } } for index in &mesh.indices { hash ^= u64::from(*index); hash = hash.wrapping_mul(0x0000_0100_0000_01b3); } hash } #[test] fn model_workflow_asset_round_trips_and_falls_back_deterministically() { let mut model = ModelPrim::new().expect("model prim"); let mut face = ModelFace::new().expect("model face"); face.vertices = vec![ Vertex { position: Vector3 { x: -0.5, y: -0.5, z: 0.0, }, normal: Vector3::unit_z(), tex_coord: Vector2::zero(), }, Vertex { position: Vector3 { x: 0.5, y: -0.5, z: 0.0, }, normal: Vector3::unit_z(), tex_coord: Vector2 { x: 1.0, y: 0.0 }, }, Vertex { position: Vector3 { x: -0.5, y: 0.5, z: 0.0, }, normal: Vector3::unit_z(), tex_coord: Vector2 { x: 0.0, y: 1.0 }, }, ]; face.indices = vec![0, 1, 2]; model.faces.push(face); model.create_asset(UUID::zero()).expect("mesh asset"); let renderer = MeshFoundry::new().expect("renderer"); let primitive = Primitive::new_with_constructor().expect("primitive"); let highest = renderer .generate_faceted_mesh_mesh_with_primitive_bytes_detail_level( primitive.clone(), model.asset.clone(), DetailLevel::Highest, ) .expect("highest decode") .expect("highest mesh"); let fallback = renderer .generate_faceted_mesh_mesh_with_primitive_bytes_detail_level( primitive, model.asset.clone(), DetailLevel::Low, ) .expect("fallback decode") .expect("fallback mesh"); assert_eq!(highest.faces.len(), 1); assert_eq!(highest.faces[0].vertices.len(), 3); assert_eq!(highest.faces[0].indices, [0, 1, 2]); assert_eq!(fallback.faces[0].indices, highest.faces[0].indices); let aux = highest.faces[0] .user_data .downcast_ref::() .expect("generated tangent domain"); assert_eq!(aux.tangents.len(), 3); assert!(aux.tangents.iter().all(|tangent| tangent.w.abs() == 1.0)); let unpacked = renderer .unpack_mesh(model.asset) .expect("unpack call") .expect("unpacked map"); assert!(matches!(unpacked.get("high_lod"), Some(OSD::Array(_)))); assert!(matches!(unpacked.get("physics_convex"), Some(OSD::Map(_)))); } #[test] fn rigged_mesh_preserves_skin_uv_tangent_material_and_ordering_domains() { let texture = UUID::new_with_string("11111111-2222-3333-4444-555555555555".into()).expect("texture UUID"); let material = UUID::new_with_string("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into()) .expect("material UUID"); let mut textures = PrimitiveTextureEntry::new_with_uuid(texture).expect("texture entry"); textures .create_face(0) .expect("face override") .set_render_material_id(material); let mut primitive = Primitive::new_with_constructor().expect("primitive"); primitive.textures = Some(textures); let renderer = MeshFoundry::new().expect("renderer"); let mesh = renderer .generate_faceted_mesh_mesh_with_primitive_bytes( primitive, rigged_asset([0, 1, 2], weights()), ) .expect("rigged decode") .expect("rigged mesh"); let skin = mesh.skin_data.as_ref().expect("skin data"); assert_eq!(skin.joint_names, ["mPelvis", "mTorso"]); assert_eq!(skin.inverse_bind_matrices.len(), 32); assert_eq!(skin.alt_inverse_bind_matrices.len(), 32); assert_eq!(skin.bind_shape_matrix.len(), 16); assert_eq!(skin.pelvis_offset, 0.25); assert!(skin.lock_scale_if_joint_position); assert_eq!( RiggedSkinMath::build_inv_bind_matrices(skin.clone()) .expect("decoded inverse bind matrices") .len(), 2 ); assert_eq!( RiggedSkinMath::extract_joint_position_overrides(skin.clone()) .expect("decoded joint overrides") .len(), 2 ); let face = &mesh.faces[0]; assert_eq!(face.indices, [0, 1, 2]); assert_eq!( face.normalized_scale, Vector3 { x: 2.0, y: 3.0, z: 4.0 } ); assert_eq!(face.texture_face.texture_id(), texture); assert_eq!(face.texture_face.render_material_id(), material); assert_eq!(face.tex_coords1.as_ref().expect("UV1").len(), 3); let weights = face.weights.as_ref().expect("weights"); assert_eq!(weights.len(), 3); for weight in weights { let total = weight.weight0 + weight.weight1 + weight.weight2 + weight.weight3; assert!((total - 1.0).abs() < 1.0e-6); } assert_eq!(weights[0].joint0, 0); assert_eq!(weights[0].joint1, 1); assert_eq!(weights[2].weight0, 1.0); let tangents = &face .user_data .downcast_ref::() .expect("stored tangents") .tangents; assert_eq!(tangents.len(), 3); assert!( tangents .iter() .all(|tangent| tangent.x > 0.999 && tangent.w == -1.0) ); } #[test] fn compressed_submesh_decode_has_stable_golden_output() { let section = compress(OSD::Array(vec![rigged_submesh([0, 1, 2], Vec::new())])); let renderer = MeshFoundry::new().expect("renderer"); let mesh = renderer .mesh_sub_mesh_as_simple_mesh( Primitive::new_with_constructor().expect("primitive"), section, ) .expect("submesh decode") .expect("simple mesh"); assert_eq!(mesh_hash(&mesh), 0xcb32_07be_6dea_73c8); } #[test] fn compressed_submesh_decode_rebases_indices_in_face_order() { let section = compress(OSD::Array(vec![ rigged_submesh([0, 1, 2], Vec::new()), rigged_submesh([2, 1, 0], Vec::new()), ])); let mesh = MeshFoundry::new() .expect("renderer") .mesh_sub_mesh_as_simple_mesh( Primitive::new_with_constructor().expect("primitive"), section, ) .expect("submesh decode") .expect("simple mesh"); assert_eq!(mesh.vertices.len(), 6); assert_eq!(mesh.indices, [0, 1, 2, 5, 4, 3]); } #[test] fn convex_hulls_decode_bounding_and_decomposed_domains() { let positions = quantized3( &[[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, 0.5]], [-0.5; 3], [0.5; 3], ); let section = compress(OSD::Map(HashMap::from([ ( "Min".to_owned(), OSD::from_vector3(Vector3 { x: -0.5, y: -0.5, z: -0.5, }) .expect("min"), ), ( "Max".to_owned(), OSD::from_vector3(Vector3 { x: 0.5, y: 0.5, z: 0.5, }) .expect("max"), ), ("BoundingVerts".to_owned(), OSD::Binary(positions.clone())), ("HullList".to_owned(), OSD::Binary(vec![3])), ("Positions".to_owned(), OSD::Binary(positions)), ]))); let renderer = MeshFoundry::new().expect("renderer"); let primitive = Primitive::new_with_constructor().expect("primitive"); let mut bounding = Vec::new(); let hulls = renderer .mesh_sub_mesh_as_convex_hulls_with_primitive_bytes_list( primitive.clone(), section.clone(), &mut bounding, ) .expect("convex decode"); assert_eq!(bounding.len(), 3); assert_eq!(hulls.len(), 1); assert_eq!(hulls[0], bounding); assert_eq!( renderer .mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(primitive, section) .expect("convex overload"), hulls ); } #[test] fn malformed_offsets_indices_and_weights_return_source_context() { let source = UUID::new_with_string("12345678-1234-5678-9abc-def012345678".into()).expect("source UUID"); let mut primitive = Primitive::new_with_constructor().expect("primitive"); primitive.id = source; let renderer = MeshFoundry::new().expect("renderer"); let invalid_index = renderer.generate_faceted_mesh_mesh_with_primitive_bytes( primitive.clone(), rigged_asset([0, 1, 3], weights()), ); assert!(matches!( invalid_index, Err(Error::Rendering { source: actual, context: "mesh triangle index is out of range" }) if actual == source )); let invalid_weights = renderer.generate_faceted_mesh_mesh_with_primitive_bytes( primitive.clone(), rigged_asset([0, 1, 2], vec![2, 0, 128, 0xff, 0xff, 0xff]), ); assert!(matches!( invalid_weights, Err(Error::Rendering { source: actual, context: "mesh skin joint index is out of range" }) if actual == source )); let header = OSD::Map(HashMap::from([( "high_lod".to_owned(), OSD::Map(HashMap::from([ ("offset".to_owned(), OSD::Integer(i32::MAX)), ("size".to_owned(), OSD::Integer(64)), ])), )])); let invalid_offset = OSDParser::serialize_llsd_binary_with_osd_boolean(header, false) .expect("invalid fixture header"); assert!(matches!( renderer.generate_faceted_mesh_mesh_with_primitive_bytes(primitive, invalid_offset), Err(Error::Rendering { source: actual, context: "mesh section is outside the asset" }) if actual == source )); } #[test] fn mesh_sculpt_mirror_and_invert_update_coordinates_normals_and_tangents() { let mut primitive = Primitive::new_with_constructor().expect("primitive"); let mut bytes = UUID::zero().get_bytes().expect("UUID bytes"); bytes.push(SculptType::Mesh as u8 | SculptType::Mirror as u8 | SculptType::Invert as u8); primitive.sculpt = Some(PrimitiveSculptData::new_with_bytes_int32(bytes, 0).expect("sculpt flags")); let mesh = MeshFoundry::new() .expect("renderer") .generate_faceted_mesh_mesh_with_primitive_bytes( primitive, rigged_asset([0, 1, 2], weights()), ) .expect("modified decode") .expect("modified mesh"); let face = &mesh.faces[0]; // Mirror and invert each reverse winding, so applying both preserves it. assert_eq!(face.indices, [0, 1, 2]); assert_eq!(face.vertices[0].position.x, 0.5); assert!(face.vertices[0].normal.z < -0.999); let tangents = &face .user_data .downcast_ref::() .expect("tangents") .tangents; assert!(tangents[0].x < -0.999); assert_eq!(tangents[0].w, -1.0); }