Implement MeshFoundry pipeline (#77)
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
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
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
#![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);
|
||||
}
|
||||
Reference in New Issue
Block a user