Files
Chili Palmer 88408c9680
Some checks failed
Native code generation / deterministic (push) Failing after 1m58s
Imaging and meshing gate / native (push) Failing after 4m10s
JPEG 2000 feature / linux (push) Successful in 3m0s
Native Rust workspace compile / compile (push) Failing after 6m12s
Skia feature / linux (push) Has been cancelled
Implement MeshFoundry pipeline (#77)
2026-08-10 22:37:43 +00:00

571 lines
18 KiB
Rust

#![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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8>) -> 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<u8> = tangents
.iter()
.flat_map(|value| {
value
.iter()
.flat_map(|component| quantize(*component, -1.0, 1.0).to_le_bytes())
})
.collect();
let triangle_bytes: Vec<u8> = 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<u8> {
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<u8>) -> Vec<u8> {
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::<MeshFaceAux>()
.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::<MeshFaceAux>()
.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::<MeshFaceAux>()
.expect("tangents")
.tangents;
assert!(tangents[0].x < -0.999);
assert_eq!(tangents[0].w, -1.0);
}