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:
@@ -8,6 +8,7 @@ on:
|
|||||||
- "tools/check_milestone_09.py"
|
- "tools/check_milestone_09.py"
|
||||||
- "tools/test_milestone_09.py"
|
- "tools/test_milestone_09.py"
|
||||||
- "tools/check_milestone_10_issue_76.py"
|
- "tools/check_milestone_10_issue_76.py"
|
||||||
|
- "tools/check_milestone_10_issue_77.py"
|
||||||
- "**/*.rs"
|
- "**/*.rs"
|
||||||
- "**/Cargo.toml"
|
- "**/Cargo.toml"
|
||||||
- "Cargo.lock"
|
- "Cargo.lock"
|
||||||
@@ -18,6 +19,7 @@ on:
|
|||||||
- "tools/check_milestone_09.py"
|
- "tools/check_milestone_09.py"
|
||||||
- "tools/test_milestone_09.py"
|
- "tools/test_milestone_09.py"
|
||||||
- "tools/check_milestone_10_issue_76.py"
|
- "tools/check_milestone_10_issue_76.py"
|
||||||
|
- "tools/check_milestone_10_issue_77.py"
|
||||||
- "**/*.rs"
|
- "**/*.rs"
|
||||||
- "**/Cargo.toml"
|
- "**/Cargo.toml"
|
||||||
- "Cargo.lock"
|
- "Cargo.lock"
|
||||||
@@ -42,7 +44,7 @@ jobs:
|
|||||||
tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX"
|
tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX"
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Audit native milestone 09 boundaries
|
- name: Audit native implementation boundaries
|
||||||
run: |
|
run: |
|
||||||
python3 tools/check_milestone_09_issue_59.py
|
python3 tools/check_milestone_09_issue_59.py
|
||||||
python3 tools/check_milestone_09_issue_60.py
|
python3 tools/check_milestone_09_issue_60.py
|
||||||
@@ -54,6 +56,7 @@ jobs:
|
|||||||
python3 tools/check_milestone_09_issue_66.py
|
python3 tools/check_milestone_09_issue_66.py
|
||||||
python3 tools/check_milestone_09.py
|
python3 tools/check_milestone_09.py
|
||||||
python3 tools/check_milestone_10_issue_76.py
|
python3 tools/check_milestone_10_issue_76.py
|
||||||
|
python3 tools/check_milestone_10_issue_77.py
|
||||||
- name: Test the complete native world milestone
|
- name: Test the complete native world milestone
|
||||||
run: python3 tools/test_milestone_09.py
|
run: python3 tools/test_milestone_09.py
|
||||||
- name: Compile every workspace target with bounded memory
|
- name: Compile every workspace target with bounded memory
|
||||||
|
|||||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -957,11 +957,14 @@ dependencies = [
|
|||||||
name = "libremetaverse-rendering-mesh-foundry"
|
name = "libremetaverse-rendering-mesh-foundry"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
"libremetaverse",
|
"libremetaverse",
|
||||||
"libremetaverse-imaging",
|
"libremetaverse-imaging",
|
||||||
"libremetaverse-prim-mesher",
|
"libremetaverse-prim-mesher",
|
||||||
|
"libremetaverse-rendering-simple",
|
||||||
"libremetaverse-structured-data",
|
"libremetaverse-structured-data",
|
||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
|
"stats_alloc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@@ -33,3 +33,13 @@ unsafe_code = "forbid"
|
|||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
all = { level = "warn", priority = -1 }
|
all = { level = "warn", priority = -1 }
|
||||||
pedantic = { level = "warn", priority = -1 }
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
# The generated core API is intentionally very large; full release LTO-style
|
||||||
|
# optimization can exceed small Gitea worker memory before diagnostic fixture
|
||||||
|
# benchmarks run. This stable profile retains optimization without debug or
|
||||||
|
# incremental artifacts and is used only when explicitly selected.
|
||||||
|
[profile.benchmark]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 1
|
||||||
|
debug = 0
|
||||||
|
incremental = false
|
||||||
|
|||||||
@@ -453,3 +453,12 @@ primitive UUID. It deliberately does not fetch or decode mesh assets; that
|
|||||||
surface remains with MeshFoundry. Supported topology, 16-bit geometry budgets,
|
surface remains with MeshFoundry. Supported topology, 16-bit geometry budgets,
|
||||||
determinism, and the asset boundary are documented in the
|
determinism, and the asset boundary are documented in the
|
||||||
[`SimpleRenderer` guide](crates/libremetaverse-rendering-simple/README.md).
|
[`SimpleRenderer` guide](crates/libremetaverse-rendering-simple/README.md).
|
||||||
|
|
||||||
|
The native `MeshFoundry` pipeline builds on that reference path for prims and
|
||||||
|
sculpts, and adds bounded packed-LLSD mesh assets, deterministic LOD fallback,
|
||||||
|
terrain and convex hulls, independent UV domains, generated or stored normals
|
||||||
|
and tangents, inherited material metadata, mirror/invert transforms, and
|
||||||
|
rigged-mesh skin matrices and normalized weights. A large executable fixture
|
||||||
|
reports decode allocations and timing without replacing correctness gates. The
|
||||||
|
format, ownership, error, and tangent-output contracts are documented in the
|
||||||
|
[`MeshFoundry` guide](crates/libremetaverse-rendering-mesh-foundry/README.md).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
|||||||
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |
|
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |
|
||||||
| `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims |
|
| `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims |
|
||||||
| `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim |
|
| `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim |
|
||||||
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
|
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | native implementation: 1 type / 14 members; no generated shims remain |
|
||||||
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | native implementation: 1 type / 6 members; no generated shims remain |
|
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | native implementation: 1 type / 6 members; no generated shims remain |
|
||||||
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 16 types / 295 members; no generated shims remain |
|
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 16 types / 295 members; no generated shims remain |
|
||||||
| `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |
|
| `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |
|
||||||
|
|||||||
@@ -5,14 +5,23 @@ edition.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
repository.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]
|
[dependencies]
|
||||||
|
flate2 = "1.1.2"
|
||||||
libremetaverse = { path = "../libremetaverse" }
|
libremetaverse = { path = "../libremetaverse" }
|
||||||
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
|
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
|
||||||
libremetaverse-prim-mesher = { path = "../libremetaverse-prim-mesher" }
|
libremetaverse-prim-mesher = { path = "../libremetaverse-prim-mesher" }
|
||||||
|
libremetaverse-rendering-simple = { path = "../libremetaverse-rendering-simple" }
|
||||||
libremetaverse-structured-data = { path = "../libremetaverse-structured-data" }
|
libremetaverse-structured-data = { path = "../libremetaverse-structured-data" }
|
||||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
stats_alloc = "0.1.10"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "mesh_decode"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
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)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
/// C# type: `T:LibreMetaverse.Rendering.MeshFoundry`.
|
/// C# type: `T:LibreMetaverse.Rendering.MeshFoundry`.
|
||||||
pub struct MeshFoundry;
|
pub use crate::mesh_foundry::MeshFoundry;
|
||||||
impl MeshFoundry {
|
impl MeshFoundry {
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.#ctor`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.#ctor`.
|
||||||
pub fn new() -> Result<Self, crate::Error> {
|
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)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_faceted_mesh(
|
pub fn generate_faceted_mesh(
|
||||||
@@ -18,9 +18,7 @@ impl MeshFoundry {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_faceted_mesh(prim, lod)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])`.
|
||||||
pub fn generate_faceted_mesh_mesh_with_primitive_bytes(
|
pub fn generate_faceted_mesh_mesh_with_primitive_bytes(
|
||||||
@@ -28,8 +26,10 @@ impl MeshFoundry {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
mesh_data: Vec<u8>,
|
mesh_data: Vec<u8>,
|
||||||
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_faceted_mesh_mesh(
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])",
|
prim,
|
||||||
|
mesh_data,
|
||||||
|
libremetaverse::rendering::DetailLevel::Highest,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
@@ -39,9 +39,7 @@ impl MeshFoundry {
|
|||||||
mesh_data: Vec<u8>,
|
mesh_data: Vec<u8>,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_faceted_mesh_mesh(prim, mesh_data, lod)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_faceted_sculpt_mesh(
|
pub fn generate_faceted_sculpt_mesh(
|
||||||
@@ -50,9 +48,7 @@ impl MeshFoundry {
|
|||||||
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_simple_mesh(
|
pub fn generate_simple_mesh(
|
||||||
@@ -60,9 +56,7 @@ impl MeshFoundry {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_simple_mesh(prim, lod, false)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_simple_mesh_with_normals(
|
pub fn generate_simple_mesh_with_normals(
|
||||||
@@ -70,9 +64,7 @@ impl MeshFoundry {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_simple_mesh(prim, lod, true)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_simple_sculpt_mesh(
|
pub fn generate_simple_sculpt_mesh(
|
||||||
@@ -81,9 +73,7 @@ impl MeshFoundry {
|
|||||||
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])`.
|
||||||
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(
|
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(
|
||||||
@@ -91,9 +81,7 @@ impl MeshFoundry {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
compressed_mesh_data: Vec<u8>,
|
compressed_mesh_data: Vec<u8>,
|
||||||
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
|
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, None)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)`.
|
/// 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(
|
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes_list(
|
||||||
@@ -102,9 +90,7 @@ impl MeshFoundry {
|
|||||||
compressed_mesh_data: Vec<u8>,
|
compressed_mesh_data: Vec<u8>,
|
||||||
bounding_hull: &mut Vec<libremetaverse_types::Vector3>,
|
bounding_hull: &mut Vec<libremetaverse_types::Vector3>,
|
||||||
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
|
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, Some(bounding_hull))
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])`.
|
||||||
pub fn mesh_sub_mesh_as_simple_mesh(
|
pub fn mesh_sub_mesh_as_simple_mesh(
|
||||||
@@ -112,9 +98,7 @@ impl MeshFoundry {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
compressed_mesh_data: Vec<u8>,
|
compressed_mesh_data: Vec<u8>,
|
||||||
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_mesh_sub_mesh_as_simple_mesh(prim, compressed_mesh_data)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)`.
|
||||||
pub fn terrain_mesh(
|
pub fn terrain_mesh(
|
||||||
@@ -125,9 +109,7 @@ impl MeshFoundry {
|
|||||||
y_begin: f32,
|
y_begin: f32,
|
||||||
y_end: f32,
|
y_end: f32,
|
||||||
) -> Result<libremetaverse::rendering::Face, crate::Error> {
|
) -> Result<libremetaverse::rendering::Face, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_terrain_mesh(z_map, x_begin, x_end, y_begin, y_end)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)`.
|
/// 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(
|
pub fn transform_tex_coords(
|
||||||
@@ -137,18 +119,13 @@ impl MeshFoundry {
|
|||||||
te_face: libremetaverse::PrimitiveTextureEntryFace,
|
te_face: libremetaverse::PrimitiveTextureEntryFace,
|
||||||
prim_scale: libremetaverse_types::Vector3,
|
prim_scale: libremetaverse_types::Vector3,
|
||||||
) -> Result<(), crate::Error> {
|
) -> Result<(), crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])`.
|
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])`.
|
||||||
pub fn unpack_mesh(
|
pub fn unpack_mesh(
|
||||||
&self,
|
&self,
|
||||||
asset_data: Vec<u8>,
|
asset_data: Vec<u8>,
|
||||||
) -> Result<Option<libremetaverse_structured_data::OSDMap>, crate::Error> {
|
) -> Result<Option<libremetaverse_structured_data::OSDMap>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_unpack_mesh(asset_data).map(Some)
|
||||||
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
extern crate self as libremetaverse_rendering_mesh_foundry;
|
||||||
|
|
||||||
mod generated;
|
mod generated;
|
||||||
|
mod mesh_foundry;
|
||||||
|
|
||||||
pub use generated::*;
|
pub use generated::*;
|
||||||
pub use libremetaverse as core;
|
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_structured_data as structured_data;
|
||||||
pub use libremetaverse_types as types;
|
pub use libremetaverse_types as types;
|
||||||
pub use libremetaverse_types::Error;
|
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);
|
||||||
|
}
|
||||||
@@ -21,10 +21,14 @@ const fn is_binary_whitespace(byte: u8) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
|
pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
|
||||||
|
deserialize_prefix(&data).map(|(value, _)| value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn deserialize_prefix(data: &[u8]) -> Result<(OSD, usize), Error> {
|
||||||
if data.len() > MAX_INPUT_BYTES {
|
if data.len() > MAX_INPUT_BYTES {
|
||||||
return Err(parse_error(0, "binary LLSD input exceeds allocation limit"));
|
return Err(parse_error(0, "binary LLSD input exceeds allocation limit"));
|
||||||
}
|
}
|
||||||
let mut parser = Parser::new(&data);
|
let mut parser = Parser::new(data);
|
||||||
parser.skip_whitespace();
|
parser.skip_whitespace();
|
||||||
if parser.consume_prefix_ignore_ascii_case(ALT_HEADER)
|
if parser.consume_prefix_ignore_ascii_case(ALT_HEADER)
|
||||||
|| parser.consume_prefix_ignore_ascii_case(HEADER)
|
|| parser.consume_prefix_ignore_ascii_case(HEADER)
|
||||||
@@ -37,7 +41,7 @@ pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
|
|||||||
OSD::DEFAULT_MAX_NODES,
|
OSD::DEFAULT_MAX_NODES,
|
||||||
OSD::DEFAULT_MAX_BINARY_BYTES,
|
OSD::DEFAULT_MAX_BINARY_BYTES,
|
||||||
)?;
|
)?;
|
||||||
Ok(value)
|
Ok((value, parser.position))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn deserialize_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<OSD, Error> {
|
pub(crate) fn deserialize_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<OSD, Error> {
|
||||||
@@ -596,6 +600,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefix_decode_reports_exact_payload_boundary() {
|
||||||
|
let value = OSD::Map(HashMap::from([("offset".into(), OSD::Integer(7))]));
|
||||||
|
let encoded = serialize_with_header(value.clone(), false).unwrap();
|
||||||
|
let mut container = encoded.clone();
|
||||||
|
container.extend_from_slice(b"\x78\x9c appended payload");
|
||||||
|
let (decoded, consumed) = deserialize_prefix(&container).unwrap();
|
||||||
|
assert_eq!(decoded, value);
|
||||||
|
assert_eq!(consumed, encoded.len());
|
||||||
|
assert_eq!(&container[consumed..], b"\x78\x9c appended payload");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_seed_corpus_is_rejected_with_positions() {
|
fn malformed_seed_corpus_is_rejected_with_positions() {
|
||||||
let expected: &[(&str, &str)] = &[
|
let expected: &[(&str, &str)] = &[
|
||||||
|
|||||||
@@ -54,3 +54,15 @@ pub mod xml {
|
|||||||
pub use generated::*;
|
pub use generated::*;
|
||||||
pub use libremetaverse_types as types;
|
pub use libremetaverse_types as types;
|
||||||
pub use libremetaverse_types::Error;
|
pub use libremetaverse_types::Error;
|
||||||
|
|
||||||
|
/// Decodes one bounded binary LLSD value and returns the number of consumed
|
||||||
|
/// bytes, allowing native container formats to locate payloads appended after
|
||||||
|
/// an LLSD header without duplicating the codec.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a positioned parse error when the prefix is malformed, truncated,
|
||||||
|
/// or exceeds the shared LLSD depth, node, input, or allocation bounds.
|
||||||
|
pub fn deserialize_llsd_binary_prefix(data: &[u8]) -> Result<(OSD, usize), Error> {
|
||||||
|
binary::deserialize_prefix(data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ Milestone 06 owns the native implementations in `libremetaverse-imaging`,
|
|||||||
imaging and meshing crates contain no CLR bridge, subprocess adapter, foreign
|
imaging and meshing crates contain no CLR bridge, subprocess adapter, foreign
|
||||||
RPC, or generated failure-only shim. The rendering crates consume the checked
|
RPC, or generated failure-only shim. The rendering crates consume the checked
|
||||||
mesh data but remain a separate milestone: `SimpleRenderer` now provides the
|
mesh data but remain a separate milestone: `SimpleRenderer` now provides the
|
||||||
reference prim/sculpt conversion pipeline, while mesh-asset decoding and
|
reference prim/sculpt conversion pipeline. `MeshFoundry` now adds bounded mesh
|
||||||
rigging remain owned by the separate `MeshFoundry` issue. See the
|
asset, rigging, tangent, secondary-UV, terrain, and convex-hull handling without
|
||||||
|
changing the milestone 06 ownership boundary. See the
|
||||||
[`SimpleRenderer` guide](../crates/libremetaverse-rendering-simple/README.md)
|
[`SimpleRenderer` guide](../crates/libremetaverse-rendering-simple/README.md)
|
||||||
for that boundary.
|
and [`MeshFoundry` guide](../crates/libremetaverse-rendering-mesh-foundry/README.md).
|
||||||
|
|
||||||
## Feature boundary
|
## Feature boundary
|
||||||
|
|
||||||
|
|||||||
109
tools/check_milestone_10_issue_77.py
Normal file
109
tools/check_milestone_10_issue_77.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit issue 77's native MeshFoundry ownership and evidence boundary."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
import generate_api_shims
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SOURCE = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "src" / "mesh_foundry.rs"
|
||||||
|
GENERATED = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "src" / "generated.rs"
|
||||||
|
MANIFEST = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "Cargo.toml"
|
||||||
|
ASSET_TESTS = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "tests" / "mesh_assets.rs"
|
||||||
|
COMPAT = ROOT / "tests" / "compat" / "tests" / "imaging_meshing_semantics.rs"
|
||||||
|
BENCH = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "benches" / "mesh_decode.rs"
|
||||||
|
WORKSPACE = ROOT / "Cargo.toml"
|
||||||
|
DOC = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "README.md"
|
||||||
|
STRUCTURED = ROOT / "crates" / "libremetaverse-structured-data" / "src" / "lib.rs"
|
||||||
|
WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml"
|
||||||
|
STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(")
|
||||||
|
|
||||||
|
TYPE = "T:LibreMetaverse.Rendering.MeshFoundry"
|
||||||
|
MEMBERS = {
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.#ctor",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def require_markers(path: Path, markers: tuple[str, ...]) -> None:
|
||||||
|
text = path.read_text()
|
||||||
|
missing = [marker for marker in markers if marker not in text]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if generate_api_shims.NATIVE_DECLARATIONS.get(TYPE) != "crate::mesh_foundry::MeshFoundry":
|
||||||
|
raise SystemExit("issue 77 MeshFoundry native declaration is missing")
|
||||||
|
missing = sorted(MEMBERS - set(generate_api_shims.NATIVE_MEMBER_BODIES))
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("issue 77 native members missing: " + ", ".join(missing))
|
||||||
|
for source in (SOURCE, GENERATED):
|
||||||
|
if STUB_RE.search(source.read_text()):
|
||||||
|
raise SystemExit(f"issue 77 owned Rust stubs remain in {source.name}")
|
||||||
|
|
||||||
|
require_markers(SOURCE, (
|
||||||
|
"impl IRendering for MeshFoundry", "SimpleRenderer", "SculptMesh::from_height_rows",
|
||||||
|
"deserialize_llsd_binary_prefix", "ZlibDecoder", "select_lod", "decode_submesh",
|
||||||
|
"TexCoord1", "Tangent", "generate_normals", "generate_tangents", "MeshFaceAux",
|
||||||
|
"decode_skin", "decode_weights", "apply_sculpt_modifiers", "texture_for_face",
|
||||||
|
"MAX_ASSET_BYTES", "MAX_TOTAL_VERTICES", "MAX_JOINTS", "checked_add",
|
||||||
|
"mesh triangle index is out of range", "Error::Rendering",
|
||||||
|
))
|
||||||
|
manifest = MANIFEST.read_text()
|
||||||
|
required_deps = (
|
||||||
|
"flate2", "libremetaverse-prim-mesher", "libremetaverse-rendering-simple",
|
||||||
|
"libremetaverse-structured-data", "stats_alloc",
|
||||||
|
)
|
||||||
|
missing_deps = [dependency for dependency in required_deps if dependency not in manifest]
|
||||||
|
if missing_deps:
|
||||||
|
raise SystemExit("issue 77 dependencies/evidence missing: " + ", ".join(missing_deps))
|
||||||
|
|
||||||
|
require_markers(ASSET_TESTS, (
|
||||||
|
"model_workflow_asset_round_trips_and_falls_back_deterministically",
|
||||||
|
"rigged_mesh_preserves_skin_uv_tangent_material_and_ordering_domains",
|
||||||
|
"RiggedSkinMath::build_inv_bind_matrices", "compressed_submesh_decode_has_stable_golden_output",
|
||||||
|
"0xcb32_07be_6dea_73c8", "rebases_indices_in_face_order",
|
||||||
|
"convex_hulls_decode_bounding_and_decomposed_domains",
|
||||||
|
"malformed_offsets_indices_and_weights_return_source_context",
|
||||||
|
"mirror_and_invert_update_coordinates_normals_and_tangents",
|
||||||
|
))
|
||||||
|
require_markers(COMPAT, (
|
||||||
|
"reviewed_meshfoundrytests_generatesimplemesh_box_returnsnonnull",
|
||||||
|
"reviewed_meshfoundrytests_generatefacetedmesh_box_has6faces",
|
||||||
|
"reviewed_meshfoundrytests_transformtexcoords_identitytransform_preservesuvs",
|
||||||
|
"reviewed_meshfoundrytests_terrainmesh_allindicesinrange",
|
||||||
|
))
|
||||||
|
require_markers(BENCH, ("fixture(240)", "57_600", "342_726", "Region::new"))
|
||||||
|
require_markers(WORKSPACE, ("[profile.benchmark]", 'opt-level = 1', "incremental = false"))
|
||||||
|
require_markers(STRUCTURED, ("pub fn deserialize_llsd_binary_prefix", "binary::deserialize_prefix"))
|
||||||
|
require_markers(DOC, (
|
||||||
|
"prim-local coordinates", "MeshFaceAux", "bitangent", "UV1", "RiggedSkinMath",
|
||||||
|
"exactly-four-influence", "XOR winding", "64 MiB", "65,536", "6,000,000",
|
||||||
|
"source primitive UUID", "57,600-vertex",
|
||||||
|
))
|
||||||
|
require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_77.py",))
|
||||||
|
print(
|
||||||
|
"issue 77 audit: native prim/sculpt/terrain and packed mesh pipelines, bounded LLSD/zlib, "
|
||||||
|
"LOD, normals/tangents/UVs, materials, rigging, convex data, tests, benchmark, and docs are present"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -504,9 +504,37 @@ NATIVE_DECLARATIONS = {
|
|||||||
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
|
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
|
||||||
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
|
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
|
||||||
"T:LibreMetaverse.Rendering.SimpleRenderer": "crate::simple_renderer::SimpleRenderer",
|
"T:LibreMetaverse.Rendering.SimpleRenderer": "crate::simple_renderer::SimpleRenderer",
|
||||||
|
"T:LibreMetaverse.Rendering.MeshFoundry": "crate::mesh_foundry::MeshFoundry",
|
||||||
}
|
}
|
||||||
|
|
||||||
NATIVE_MEMBER_BODIES = {
|
NATIVE_MEMBER_BODIES = {
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.#ctor": "Self::native_new()",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_faceted_mesh(prim, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])":
|
||||||
|
"self.native_generate_faceted_mesh_mesh(prim, mesh_data, libremetaverse::rendering::DetailLevel::Highest)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_faceted_mesh_mesh(prim, mesh_data, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_simple_mesh(prim, lod, false)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_simple_mesh(prim, lod, true)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])":
|
||||||
|
"self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, None)",
|
||||||
|
"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))",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])":
|
||||||
|
"self.native_mesh_sub_mesh_as_simple_mesh(prim, compressed_mesh_data)",
|
||||||
|
"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)",
|
||||||
|
"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)",
|
||||||
|
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])":
|
||||||
|
"self.native_unpack_mesh(asset_data).map(Some)",
|
||||||
"M:LibreMetaverse.Rendering.SimpleRenderer.#ctor": "Self::native_new()",
|
"M:LibreMetaverse.Rendering.SimpleRenderer.#ctor": "Self::native_new()",
|
||||||
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
"self.native_generate_faceted_mesh(prim, lod)",
|
"self.native_generate_faceted_mesh(prim, lod)",
|
||||||
|
|||||||
Reference in New Issue
Block a user