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:
@@ -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
|
||||
|
||||
85
crates/libremetaverse-rendering-mesh-foundry/README.md
Normal file
85
crates/libremetaverse-rendering-mesh-foundry/README.md
Normal 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.
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
1436
crates/libremetaverse-rendering-mesh-foundry/src/mesh_foundry.rs
Normal file
1436
crates/libremetaverse-rendering-mesh-foundry/src/mesh_foundry.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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