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

This commit is contained in:
2026-08-10 22:37:43 +00:00
parent 611db567a0
commit 88408c9680
17 changed files with 2402 additions and 50 deletions

View File

@@ -5,14 +5,23 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "MeshFoundry renderer shims for the MetaCrate LibreMetaverse rewrite"
description = "Bounded native MeshFoundry renderer for the MetaCrate LibreMetaverse rewrite"
[dependencies]
flate2 = "1.1.2"
libremetaverse = { path = "../libremetaverse" }
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
libremetaverse-prim-mesher = { path = "../libremetaverse-prim-mesher" }
libremetaverse-rendering-simple = { path = "../libremetaverse-rendering-simple" }
libremetaverse-structured-data = { path = "../libremetaverse-structured-data" }
libremetaverse-types = { path = "../libremetaverse-types" }
[dev-dependencies]
stats_alloc = "0.1.10"
[[bench]]
name = "mesh_decode"
harness = false
[lints]
workspace = true

View File

@@ -0,0 +1,85 @@
# LibreMetaverse MeshFoundry renderer
`libremetaverse-rendering-mesh-foundry` is the bounded native renderer for
legacy prims, sculpt maps, terrain, and Second Life LLSD mesh assets. It uses
the checked SimpleRenderer/PrimMesher path for legacy geometry and the native
StructuredData binary codec for mesh containers. It does not wrap, load, or
invoke the C# renderer or duplicate the LLSD and image codecs.
## Pipelines
The mapped `MeshFoundry` methods cover:
- simple and face-grouped legacy prim geometry at every `DetailLevel`, always
retaining finite normals and UVs;
- plane, cylinder, sphere, and torus sculpt maps;
- rectangular height-map terrain with generated normals and UVs;
- packed mesh-asset headers and individually zlib-compressed LLSD sections;
- requested visual LOD selection (`high_lod`, `medium_lod`, `low_lod`, and
`lowest_lod`) with deterministic highest-to-lowest fallback;
- single-section simple-mesh extraction and physics convex/bounding hulls; and
- default or planar texture transforms, repeats, offsets, rotation, and prim
scale through the common `IRendering` contract.
Mesh vertices remain in prim-local coordinates. Quantized positions use each
submesh's `PositionDomain`; normals and tangent components use the format's
fixed `[-1, 1]` domain; UV0 and optional UV1 use their independent domains.
Triangle and face order is preserved, while flattened multi-face output rebases
indices without reordering vertices. Missing normals and tangents are generated
deterministically from triangles and UVs. Degenerate UV gradients receive a
stable normal-orthogonal fallback tangent.
The mapped `Face` type has no tangent field. Mesh tangents are therefore kept
without changing the public C# layout in a public `MeshFaceAux` value stored in
`Face.user_data`; recover it with
`face.user_data.downcast_ref::<MeshFaceAux>()`. Its `Vector4.w` is the bitangent
handedness. UV1 remains in `Face.tex_coords1`.
## Rigged meshes and materials
The `skin` section decodes bounded joint names, inverse-bind matrices,
bind-shape matrix, alternative inverse-bind matrices, pelvis offset, and the
joint-position scale lock into the shared `MeshSkinData`. Per-vertex weight
streams accept the protocol's sentinel form and its exactly-four-influence
form, validate every joint index, clamp protocol weights, normalize four
influences, and bind missing vertices to joint zero. The output is directly
consumable by `RiggedSkinMath` and `AnimeshSkinning`.
Each submesh retains its original face ID and the effective inherited
`PrimitiveTextureEntryFace`, including texture, legacy material, and render
material overrides. `NormalizedScale`, face bounds, centers, optional UV1,
weights, normals, tangents, and winding are kept together. Mesh sculpt mirror
and invert flags apply the viewer-compatible reflection, normal inversion, and
XOR winding rule to every face and tangent domain.
## Bounds and failure contract
Assets and compressed/decompressed sections are capped at 64 MiB. Header
sections and submeshes are capped at 256, each face remains within the 16-bit
65,536-vertex index domain, aggregate visual geometry is capped at 1,000,000
vertices and 6,000,000 indices, skins at 512 joints, and convex data at 65,536
hulls and 1,000,000 vertices. All sizes, offsets, index rebasing, buffer strides,
and allocation products use checked arithmetic before slicing or allocation.
Malformed LLSD, zlib data, domains, parallel buffers, matrices, weights,
offsets, and indices return `Error::Rendering` with the source primitive UUID
and a stable operation context. `UnpackMesh` has no primitive parameter and
uses the zero UUID. This crate performs no asset download, scene traversal,
world transform, GPU upload, material shading, texture fetch, or drawing.
## Reproducible gates and benchmark
```console
python3 tools/check_milestone_10_issue_77.py
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics --locked -j1
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-rendering-mesh-foundry --locked -j1
CARGO_BUILD_JOBS=1 cargo bench -p libremetaverse-rendering-mesh-foundry --bench mesh_decode --profile benchmark --locked -j1
```
The named `benchmark` profile uses bounded-memory moderate optimization because
full release optimization of the generated core surface can exceed small
Gitea-worker memory before the fixture runs. The non-statistical benchmark
builds and decodes a deterministic 57,600-vertex,
342,726-index mesh asset and reports elapsed time plus allocation counts. It is
diagnostic evidence for regressions; correctness and malformed-input tests
remain the release gates.

View File

@@ -0,0 +1,82 @@
#![allow(clippy::cast_precision_loss)] // The benchmark grid is fixed at 240×240.
use std::alloc::System;
use std::hint::black_box;
use std::time::Instant;
use libremetaverse::Primitive;
use libremetaverse::import_export::{ModelFace, ModelPrim};
use libremetaverse::rendering::{DetailLevel, Vertex};
use libremetaverse_rendering_mesh_foundry::MeshFoundry;
use libremetaverse_types::{UUID, Vector2, Vector3};
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, StatsAlloc};
#[global_allocator]
static ALLOCATOR: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
fn fixture(side: usize) -> Vec<u8> {
let mut model = ModelPrim::new().expect("model prim");
let mut face = ModelFace::new().expect("model face");
face.vertices.reserve(side * side);
for y in 0..side {
for x in 0..side {
let u = x as f32 / (side - 1) as f32;
let v = y as f32 / (side - 1) as f32;
face.vertices.push(Vertex {
position: Vector3 {
x: u - 0.5,
y: v - 0.5,
z: (u * std::f32::consts::TAU).sin() * 0.05,
},
normal: Vector3::unit_z(),
tex_coord: Vector2 { x: u, y: v },
});
}
}
for y in 0..side - 1 {
for x in 0..side - 1 {
let a = u32::try_from(y * side + x).expect("bounded benchmark index");
let b = a + 1;
let c = a + u32::try_from(side).expect("bounded side");
let d = c + 1;
face.indices.extend_from_slice(&[a, b, c, b, d, c]);
}
}
model.faces.push(face);
model
.create_asset(UUID::zero())
.expect("bounded benchmark mesh asset");
model.asset
}
fn main() {
let asset = fixture(240);
let renderer = MeshFoundry::new().expect("MeshFoundry constructor");
let primitive = Primitive::new_with_constructor().expect("Primitive constructor");
let allocation_region = Region::new(ALLOCATOR);
let started = Instant::now();
let mesh = renderer
.generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
primitive,
asset.clone(),
DetailLevel::Highest,
)
.expect("large fixture decode")
.expect("large fixture geometry");
let elapsed = started.elapsed();
let allocation_stats = allocation_region.change();
assert_eq!(mesh.faces.len(), 1);
assert_eq!(mesh.faces[0].vertices.len(), 57_600);
assert_eq!(mesh.faces[0].indices.len(), 342_726);
black_box((&asset, &mesh));
println!(
"mesh decode: {} compressed bytes, {} vertices, {} indices in {elapsed:?}, {} allocations, {} reallocations, {} bytes allocated",
asset.len(),
mesh.faces[0].vertices.len(),
mesh.faces[0].indices.len(),
allocation_stats.allocations,
allocation_stats.reallocations,
allocation_stats.bytes_allocated,
);
}

View File

@@ -6,11 +6,11 @@
#![allow(non_snake_case)]
/// C# type: `T:LibreMetaverse.Rendering.MeshFoundry`.
pub struct MeshFoundry;
pub use crate::mesh_foundry::MeshFoundry;
impl MeshFoundry {
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.#ctor`.
pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Rendering.MeshFoundry.#ctor")
Self::native_new()
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_mesh(
@@ -18,9 +18,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_mesh(prim, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])`.
pub fn generate_faceted_mesh_mesh_with_primitive_bytes(
@@ -28,8 +26,10 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
mesh_data: Vec<u8>,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])",
self.native_generate_faceted_mesh_mesh(
prim,
mesh_data,
libremetaverse::rendering::DetailLevel::Highest,
)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)`.
@@ -39,9 +39,7 @@ impl MeshFoundry {
mesh_data: Vec<u8>,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_mesh_mesh(prim, mesh_data, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_sculpt_mesh(
@@ -50,9 +48,7 @@ impl MeshFoundry {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_mesh(
@@ -60,9 +56,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_mesh(prim, lod, false)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_mesh_with_normals(
@@ -70,9 +64,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_mesh(prim, lod, true)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_sculpt_mesh(
@@ -81,9 +73,7 @@ impl MeshFoundry {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])`.
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(
@@ -91,9 +81,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
compressed_mesh_data: Vec<u8>,
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])",
)
self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, None)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)`.
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes_list(
@@ -102,9 +90,7 @@ impl MeshFoundry {
compressed_mesh_data: Vec<u8>,
bounding_hull: &mut Vec<libremetaverse_types::Vector3>,
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)",
)
self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, Some(bounding_hull))
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])`.
pub fn mesh_sub_mesh_as_simple_mesh(
@@ -112,9 +98,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
compressed_mesh_data: Vec<u8>,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])",
)
self.native_mesh_sub_mesh_as_simple_mesh(prim, compressed_mesh_data)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)`.
pub fn terrain_mesh(
@@ -125,9 +109,7 @@ impl MeshFoundry {
y_begin: f32,
y_end: f32,
) -> Result<libremetaverse::rendering::Face, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)",
)
self.native_terrain_mesh(z_map, x_begin, x_end, y_begin, y_end)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)`.
pub fn transform_tex_coords(
@@ -137,18 +119,13 @@ impl MeshFoundry {
te_face: libremetaverse::PrimitiveTextureEntryFace,
prim_scale: libremetaverse_types::Vector3,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
)
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])`.
pub fn unpack_mesh(
&self,
asset_data: Vec<u8>,
) -> Result<Option<libremetaverse_structured_data::OSDMap>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])",
)
self.native_unpack_mesh(asset_data).map(Some)
}
}
impl libremetaverse::rendering::IRendering for MeshFoundry {}

View File

@@ -1,8 +1,9 @@
//! `MeshFoundry` renderer corresponding to `LibreMetaverse.Rendering.MeshFoundry`.
//! Bounded native prim, sculpt, terrain, and LLSD mesh-asset rendering.
extern crate self as libremetaverse_rendering_mesh_foundry;
mod generated;
mod mesh_foundry;
pub use generated::*;
pub use libremetaverse as core;
@@ -11,3 +12,4 @@ pub use libremetaverse_prim_mesher as prim_mesher;
pub use libremetaverse_structured_data as structured_data;
pub use libremetaverse_types as types;
pub use libremetaverse_types::Error;
pub use mesh_foundry::MeshFaceAux;

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}