Implement native SimpleRenderer reference pipeline (#76)
Some checks failed
Native code generation / deterministic (push) Failing after 2m7s
Imaging and meshing gate / native (push) Failing after 4m9s
JPEG 2000 feature / linux (push) Successful in 2m55s
Native Rust workspace compile / compile (push) Failing after 6m15s
Skia feature / linux (push) Successful in 30m59s
Some checks failed
Native code generation / deterministic (push) Failing after 2m7s
Imaging and meshing gate / native (push) Failing after 4m9s
JPEG 2000 feature / linux (push) Successful in 2m55s
Native Rust workspace compile / compile (push) Failing after 6m15s
Skia feature / linux (push) Successful in 30m59s
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
|||||||
- "tools/install_openjpeg_2_5_4.sh"
|
- "tools/install_openjpeg_2_5_4.sh"
|
||||||
- "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"
|
||||||
- "**/*.rs"
|
- "**/*.rs"
|
||||||
- "**/Cargo.toml"
|
- "**/Cargo.toml"
|
||||||
- "Cargo.lock"
|
- "Cargo.lock"
|
||||||
@@ -16,6 +17,7 @@ on:
|
|||||||
- "tools/install_openjpeg_2_5_4.sh"
|
- "tools/install_openjpeg_2_5_4.sh"
|
||||||
- "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"
|
||||||
- "**/*.rs"
|
- "**/*.rs"
|
||||||
- "**/Cargo.toml"
|
- "**/Cargo.toml"
|
||||||
- "Cargo.lock"
|
- "Cargo.lock"
|
||||||
@@ -51,6 +53,7 @@ jobs:
|
|||||||
python3 tools/check_milestone_09_issue_65.py
|
python3 tools/check_milestone_09_issue_65.py
|
||||||
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
|
||||||
- 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
|
||||||
|
|||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -970,6 +970,7 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libremetaverse",
|
"libremetaverse",
|
||||||
"libremetaverse-imaging",
|
"libremetaverse-imaging",
|
||||||
|
"libremetaverse-prim-mesher",
|
||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -7,7 +7,7 @@ host, bind to, invoke, or ship the .NET implementation. The current
|
|||||||
stage is a compiling structural shell: crate boundaries mirror the .NET library
|
stage is a compiling structural shell: crate boundaries mirror the .NET library
|
||||||
projects, public C# type names have Rust declarations, every upstream NUnit
|
projects, public C# type names have Rust declarations, every upstream NUnit
|
||||||
invocation has a traceable catalog entry, and every sample/tool project has a
|
invocation has a traceable catalog entry, and every sample/tool project has a
|
||||||
Rust binary target. Types, StructuredData, Imaging, PrimMesher, both rendering adapters, and
|
Rust binary target. Types, StructuredData, Imaging, PrimMesher, the MeshFoundry rendering adapter, and
|
||||||
the main assembly's packet/message/asset/primitive wire-data, core
|
the main assembly's packet/message/asset/primitive wire-data, core
|
||||||
runtime/networking, avatar-facing manager, world/social/service manager, RLV,
|
runtime/networking, avatar-facing manager, world/social/service manager, RLV,
|
||||||
LSL tools, Utilities, Vivox, and WebRTC slices now have complete callable,
|
LSL tools, Utilities, Vivox, and WebRTC slices now have complete callable,
|
||||||
@@ -441,3 +441,15 @@ application forms with stable escaping, while interest-list modes and unknown
|
|||||||
simulator features are preserved through LLSD capabilities. The cache,
|
simulator features are preserved through LLSD capabilities. The cache,
|
||||||
pagination, cancellation, and compatibility contracts are documented in
|
pagination, cancellation, and compatibility contracts are documented in
|
||||||
[`docs/discovery.md`](docs/discovery.md).
|
[`docs/discovery.md`](docs/discovery.md).
|
||||||
|
|
||||||
|
### Milestone 10
|
||||||
|
|
||||||
|
The native `SimpleRenderer` is the milestone's deterministic reference
|
||||||
|
pipeline for legacy prim and decoded sculpt-map geometry. It converts checked
|
||||||
|
PrimMesher output into shared faceted and simple meshes, preserves prim-face
|
||||||
|
texture/material metadata, supplies finite normals and UVs, applies default or
|
||||||
|
planar texture transforms, and reports bounded failures with the source
|
||||||
|
primitive UUID. It deliberately does not fetch or decode mesh assets; that
|
||||||
|
surface remains with MeshFoundry. Supported topology, 16-bit geometry budgets,
|
||||||
|
determinism, and the asset boundary are documented in the
|
||||||
|
[`SimpleRenderer` guide](crates/libremetaverse-rendering-simple/README.md).
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
|||||||
| `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 | callable failure-only shim |
|
||||||
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim |
|
| `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 |
|
||||||
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |
|
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ description = "Simple renderer shims for the MetaCrate LibreMetaverse rewrite"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
libremetaverse = { path = "../libremetaverse" }
|
libremetaverse = { path = "../libremetaverse" }
|
||||||
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
|
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
|
||||||
|
libremetaverse-prim-mesher = { path = "../libremetaverse-prim-mesher" }
|
||||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
58
crates/libremetaverse-rendering-simple/README.md
Normal file
58
crates/libremetaverse-rendering-simple/README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# LibreMetaverse simple renderer
|
||||||
|
|
||||||
|
`libremetaverse-rendering-simple` is the small, deterministic reference
|
||||||
|
pipeline for geometry that is already represented by a `Primitive` or a
|
||||||
|
decoded sculpt map. It has no windowing, GPU, Skia, voice, RLV, or LSL
|
||||||
|
dependency. The only geometry backend is the workspace's checked native
|
||||||
|
`libremetaverse-prim-mesher` crate.
|
||||||
|
|
||||||
|
## Supported inputs and output
|
||||||
|
|
||||||
|
`SimpleRenderer` implements both its concrete C#-mapped methods and the shared
|
||||||
|
`IRendering` trait. It supports:
|
||||||
|
|
||||||
|
- linear and flexible paths, plus circular prim paths;
|
||||||
|
- square, circular, half-circle, and all three triangular profiles;
|
||||||
|
- profile/path cuts, hollow profiles, shear, taper, twist, skew, radius offset,
|
||||||
|
and revolutions at all four `DetailLevel` values;
|
||||||
|
- plane, cylinder, sphere, and torus sculpt maps supplied as `ManagedImage`;
|
||||||
|
- faceted output grouped by prim face, and flattened simple output that keeps
|
||||||
|
each face's independent vertex/normal/UV domain;
|
||||||
|
- per-face texture-entry inheritance, including texture, legacy material, and
|
||||||
|
render-material UUIDs; and
|
||||||
|
- default and planar texture transforms with repeat, offset, rotation, and
|
||||||
|
primitive scale.
|
||||||
|
|
||||||
|
Positions are emitted in prim-local coordinates. Every face contains finite
|
||||||
|
positions, normals, and UVs, in-range triangle indices, its local bounds and
|
||||||
|
center, and its effective texture-entry face. Identical inputs produce
|
||||||
|
byte-for-byte-identical floating-point and index sequences; the compatibility
|
||||||
|
suite pins a golden high-detail box.
|
||||||
|
|
||||||
|
## Bounds and errors
|
||||||
|
|
||||||
|
Each face and flattened mesh is limited to the 16-bit index domain (65,536
|
||||||
|
vertices), and aggregate vertex/index growth uses checked arithmetic before
|
||||||
|
allocation or conversion. Non-finite construction data, malformed mesher
|
||||||
|
domains, invalid indices, unsupported topology, and budget overflow return
|
||||||
|
`Error::Rendering`. That error includes the source primitive UUID and a static
|
||||||
|
operation context so callers can identify the object that failed. Invalid
|
||||||
|
standalone texture-transform arguments return `Error::Argument` because that
|
||||||
|
API has no primitive parameter from which to derive source context.
|
||||||
|
|
||||||
|
## Deliberate boundary
|
||||||
|
|
||||||
|
A sculpt whose type is `Mesh` names an encoded mesh asset rather than a sculpt
|
||||||
|
height map. `SimpleRenderer` does not fetch or decode assets, so that input
|
||||||
|
returns a typed `mesh sculpt requires a decoded mesh asset` error. Mesh-asset
|
||||||
|
decoding, skin weights, secondary UVs, and rigged-mesh metadata belong to the
|
||||||
|
separate MeshFoundry renderer. The simple renderer also performs no world
|
||||||
|
transform, scene traversal, material shading, texture download, or drawing.
|
||||||
|
|
||||||
|
Run the focused implementation gates with:
|
||||||
|
|
||||||
|
```console
|
||||||
|
python3 tools/check_milestone_10_issue_76.py
|
||||||
|
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rendering_shims --locked -j1
|
||||||
|
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-rendering-simple --locked -j1
|
||||||
|
```
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
/// C# type: `T:LibreMetaverse.Rendering.SimpleRenderer`.
|
/// C# type: `T:LibreMetaverse.Rendering.SimpleRenderer`.
|
||||||
pub struct SimpleRenderer;
|
pub use crate::simple_renderer::SimpleRenderer;
|
||||||
impl SimpleRenderer {
|
impl SimpleRenderer {
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.#ctor`.
|
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.#ctor`.
|
||||||
pub fn new() -> Result<Self, crate::Error> {
|
pub fn new() -> Result<Self, crate::Error> {
|
||||||
libremetaverse_types::not_implemented("M:LibreMetaverse.Rendering.SimpleRenderer.#ctor")
|
Self::native_new()
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_faceted_mesh(
|
pub fn generate_faceted_mesh(
|
||||||
@@ -18,9 +18,7 @@ impl SimpleRenderer {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
|
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_faceted_mesh(prim, lod)
|
||||||
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_faceted_sculpt_mesh(
|
pub fn generate_faceted_sculpt_mesh(
|
||||||
@@ -29,9 +27,7 @@ impl SimpleRenderer {
|
|||||||
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
|
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
|
||||||
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_simple_mesh(
|
pub fn generate_simple_mesh(
|
||||||
@@ -39,9 +35,7 @@ impl SimpleRenderer {
|
|||||||
prim: libremetaverse::Primitive,
|
prim: libremetaverse::Primitive,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
|
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_simple_mesh(prim, lod)
|
||||||
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
|
||||||
pub fn generate_simple_sculpt_mesh(
|
pub fn generate_simple_sculpt_mesh(
|
||||||
@@ -50,9 +44,7 @@ impl SimpleRenderer {
|
|||||||
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
sculpt_texture: libremetaverse_imaging::ManagedImage,
|
||||||
lod: libremetaverse::rendering::DetailLevel,
|
lod: libremetaverse::rendering::DetailLevel,
|
||||||
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
|
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
|
||||||
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)`.
|
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.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(
|
||||||
@@ -62,9 +54,6 @@ impl SimpleRenderer {
|
|||||||
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.SimpleRenderer.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl libremetaverse::rendering::IRendering for SimpleRenderer {}
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
//! Simple renderer corresponding to `LibreMetaverse.Rendering.Simple`.
|
//! Deterministic reference rendering for legacy prims and decoded sculpt maps.
|
||||||
|
//!
|
||||||
|
//! [`SimpleRenderer`] converts checked `PrimMesher` geometry into the shared
|
||||||
|
//! rendering types without a graphics backend. See the crate README for the
|
||||||
|
//! supported topology, resource bounds, and mesh-asset boundary.
|
||||||
|
|
||||||
extern crate self as libremetaverse_rendering_simple;
|
extern crate self as libremetaverse_rendering_simple;
|
||||||
|
|
||||||
mod generated;
|
mod generated;
|
||||||
|
mod simple_renderer;
|
||||||
|
|
||||||
pub use generated::*;
|
pub use generated::*;
|
||||||
pub use libremetaverse as core;
|
pub use libremetaverse as core;
|
||||||
|
|||||||
665
crates/libremetaverse-rendering-simple/src/simple_renderer.rs
Normal file
665
crates/libremetaverse-rendering-simple/src/simple_renderer.rs
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
//! Deterministic reference renderer built on the checked native prim mesher.
|
||||||
|
|
||||||
|
#![allow(clippy::cast_possible_truncation)] // Checked C# float-to-int twist conversion.
|
||||||
|
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the API mapping.
|
||||||
|
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped surface.
|
||||||
|
#![allow(clippy::needless_pass_by_value)] // Mapped value parameters are owned.
|
||||||
|
#![allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible in the C# API.
|
||||||
|
#![allow(clippy::unused_self)] // IRendering exposes these operations as instance methods.
|
||||||
|
|
||||||
|
use libremetaverse::rendering::{
|
||||||
|
DetailLevel, Face, FaceMask, FacetedMesh, IRendering, SimpleMesh, Vertex,
|
||||||
|
};
|
||||||
|
use libremetaverse::{MappingType, Primitive, PrimitiveTextureEntryFace};
|
||||||
|
use libremetaverse_imaging::ManagedImage;
|
||||||
|
use libremetaverse_prim_mesher::{
|
||||||
|
PathType, PrimMesh, SculptMesh, SculptMeshSculptType, VertexIndexer,
|
||||||
|
};
|
||||||
|
use libremetaverse_types::compat::Object;
|
||||||
|
use libremetaverse_types::{
|
||||||
|
Error, HoleType, PathCurve, ProfileCurve, SculptType, Vector2, Vector3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_FACE_VERTICES: usize = 65_536;
|
||||||
|
const MAX_TOTAL_VERTICES: usize = MAX_FACE_VERTICES;
|
||||||
|
const MAX_TOTAL_INDICES: usize = MAX_TOTAL_VERTICES * 6;
|
||||||
|
|
||||||
|
/// Small, deterministic renderer for prim and sculpt geometry.
|
||||||
|
pub struct SimpleRenderer;
|
||||||
|
|
||||||
|
impl SimpleRenderer {
|
||||||
|
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn native_generate_simple_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<SimpleMesh, Error> {
|
||||||
|
let source = prim.id;
|
||||||
|
let faceted = self.native_generate_faceted_mesh(prim, lod)?;
|
||||||
|
flatten_faces(faceted.faces, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn native_generate_simple_sculpt_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
sculpt_texture: ManagedImage,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<SimpleMesh, Error> {
|
||||||
|
let source = prim.id;
|
||||||
|
let faceted = self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)?;
|
||||||
|
flatten_faces(faceted.faces, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn native_generate_faceted_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<FacetedMesh, Error> {
|
||||||
|
let source = prim.id;
|
||||||
|
let mesh = generate_prim_mesh(&prim, lod, source)?;
|
||||||
|
let indexer = mesh
|
||||||
|
.get_vertex_indexer()
|
||||||
|
.map_err(|_| render_error(source, "index primitive viewer faces"))?
|
||||||
|
.ok_or_else(|| render_error(source, "primitive viewer faces are unavailable"))?;
|
||||||
|
let faces = indexed_faces(&prim, indexer, source)?;
|
||||||
|
Ok(FacetedMesh {
|
||||||
|
faces,
|
||||||
|
skin_data: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn native_generate_faceted_sculpt_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
sculpt_texture: ManagedImage,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<FacetedMesh, Error> {
|
||||||
|
let source = prim.id;
|
||||||
|
let sculpt = prim
|
||||||
|
.sculpt
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| render_error(source, "primitive has no sculpt metadata"))?;
|
||||||
|
let sculpt_type = match sculpt.type_() {
|
||||||
|
SculptType::Cylinder => SculptMeshSculptType::Cylinder,
|
||||||
|
SculptType::Plane => SculptMeshSculptType::Plane,
|
||||||
|
SculptType::Sphere => SculptMeshSculptType::Sphere,
|
||||||
|
SculptType::Torus => SculptMeshSculptType::Torus,
|
||||||
|
SculptType::Mesh => {
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"mesh sculpt requires a decoded mesh asset",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
SculptType::None | SculptType::Invert | SculptType::Mirror => {
|
||||||
|
return Err(render_error(source, "unsupported sculpt topology"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mesher_lod = match lod {
|
||||||
|
DetailLevel::Highest | DetailLevel::High => 32,
|
||||||
|
DetailLevel::Medium => 16,
|
||||||
|
DetailLevel::Low => 8,
|
||||||
|
};
|
||||||
|
let mesh = SculptMesh::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
|
||||||
|
sculpt_texture,
|
||||||
|
sculpt_type,
|
||||||
|
mesher_lod,
|
||||||
|
true,
|
||||||
|
sculpt.mirror(),
|
||||||
|
sculpt.invert(),
|
||||||
|
)
|
||||||
|
.map_err(|_| render_error(source, "generate bounded sculpt geometry"))?;
|
||||||
|
if mesh.coords.len() > MAX_FACE_VERTICES
|
||||||
|
|| mesh.coords.len() != mesh.normals.len()
|
||||||
|
|| mesh.coords.len() != mesh.uvs.len()
|
||||||
|
{
|
||||||
|
return Err(render_error(source, "invalid sculpt vertex domains"));
|
||||||
|
}
|
||||||
|
let vertices = mesh
|
||||||
|
.coords
|
||||||
|
.iter()
|
||||||
|
.zip(&mesh.normals)
|
||||||
|
.zip(&mesh.uvs)
|
||||||
|
.map(|((position, normal), uv)| {
|
||||||
|
checked_vertex(
|
||||||
|
Vector3 {
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
z: position.z,
|
||||||
|
},
|
||||||
|
Vector3 {
|
||||||
|
x: normal.x,
|
||||||
|
y: normal.y,
|
||||||
|
z: normal.z,
|
||||||
|
},
|
||||||
|
Vector2 { x: uv.u, y: uv.v },
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let mut indices = Vec::with_capacity(
|
||||||
|
mesh.faces
|
||||||
|
.len()
|
||||||
|
.checked_mul(3)
|
||||||
|
.ok_or_else(|| render_error(source, "sculpt index count overflow"))?,
|
||||||
|
);
|
||||||
|
for face in mesh.faces {
|
||||||
|
push_triangle(
|
||||||
|
&mut indices,
|
||||||
|
face.v1,
|
||||||
|
face.v2,
|
||||||
|
face.v3,
|
||||||
|
vertices.len(),
|
||||||
|
source,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
let texture = texture_for_face(&prim, 0, source)?;
|
||||||
|
let face = make_face(0, vertices, indices, texture, source)?;
|
||||||
|
Ok(FacetedMesh {
|
||||||
|
faces: vec![face],
|
||||||
|
skin_data: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn native_transform_tex_coords(
|
||||||
|
&self,
|
||||||
|
vertices: &mut Vec<Vertex>,
|
||||||
|
center: Vector3,
|
||||||
|
te_face: PrimitiveTextureEntryFace,
|
||||||
|
prim_scale: Vector3,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !finite3(center)
|
||||||
|
|| !finite3(prim_scale)
|
||||||
|
|| ![
|
||||||
|
te_face.repeat_u(),
|
||||||
|
te_face.repeat_v(),
|
||||||
|
te_face.offset_u(),
|
||||||
|
te_face.offset_v(),
|
||||||
|
te_face.rotation(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.all(f32::is_finite)
|
||||||
|
{
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let cosine = te_face.rotation().cos();
|
||||||
|
let sine = te_face.rotation().sin();
|
||||||
|
for vertex in vertices {
|
||||||
|
if !finite3(vertex.position) || !finite3(vertex.normal) || !finite2(vertex.tex_coord) {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
if te_face.tex_map_type() == MappingType::Planar {
|
||||||
|
let mut binormal;
|
||||||
|
let normal_x = vertex.normal.x;
|
||||||
|
if (-0.5..0.5).contains(&normal_x) {
|
||||||
|
binormal = Vector3::unit_x();
|
||||||
|
if vertex.normal.y > 0.0 {
|
||||||
|
binormal.x = -1.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binormal = Vector3::unit_y();
|
||||||
|
if normal_x < 0.0 {
|
||||||
|
binormal.y = -1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tangent = cross(binormal, vertex.normal);
|
||||||
|
let scaled = Vector3 {
|
||||||
|
x: vertex.position.x * prim_scale.x,
|
||||||
|
y: vertex.position.y * prim_scale.y,
|
||||||
|
z: vertex.position.z * prim_scale.z,
|
||||||
|
};
|
||||||
|
vertex.tex_coord.x = 0.5 + dot(binormal, scaled) * 2.0;
|
||||||
|
vertex.tex_coord.y = 0.5 - dot(tangent, scaled) * 2.0;
|
||||||
|
}
|
||||||
|
let x = vertex.tex_coord.x - 0.5;
|
||||||
|
let y = vertex.tex_coord.y - 0.5;
|
||||||
|
vertex.tex_coord.x =
|
||||||
|
(x * cosine + y * sine) * te_face.repeat_u() + te_face.offset_u() + 0.5;
|
||||||
|
vertex.tex_coord.y =
|
||||||
|
(-x * sine + y * cosine) * te_face.repeat_v() + te_face.offset_v() + 0.5;
|
||||||
|
if !finite2(vertex.tex_coord) {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IRendering for SimpleRenderer {
|
||||||
|
fn generate_faceted_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<Option<FacetedMesh>, Error> {
|
||||||
|
self.native_generate_faceted_mesh(prim, lod).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_faceted_sculpt_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
sculpt_texture: ManagedImage,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<Option<FacetedMesh>, Error> {
|
||||||
|
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_simple_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<Option<SimpleMesh>, Error> {
|
||||||
|
self.native_generate_simple_mesh(prim, lod).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_simple_sculpt_mesh(
|
||||||
|
&self,
|
||||||
|
prim: Primitive,
|
||||||
|
sculpt_texture: ManagedImage,
|
||||||
|
lod: DetailLevel,
|
||||||
|
) -> Result<Option<SimpleMesh>, Error> {
|
||||||
|
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_tex_coords(
|
||||||
|
&self,
|
||||||
|
vertices: &mut Vec<Vertex>,
|
||||||
|
center: Vector3,
|
||||||
|
te_face: PrimitiveTextureEntryFace,
|
||||||
|
prim_scale: Vector3,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_prim_mesh(
|
||||||
|
prim: &Primitive,
|
||||||
|
lod: DetailLevel,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<PrimMesh, Error> {
|
||||||
|
let data = &prim.prim_data;
|
||||||
|
let parameters = [
|
||||||
|
data.profile_begin,
|
||||||
|
data.profile_end,
|
||||||
|
data.profile_hollow,
|
||||||
|
data.path_scale_x,
|
||||||
|
data.path_scale_y,
|
||||||
|
data.path_begin,
|
||||||
|
data.path_end,
|
||||||
|
data.path_shear_x,
|
||||||
|
data.path_shear_y,
|
||||||
|
data.path_radius_offset,
|
||||||
|
data.path_revolutions,
|
||||||
|
data.path_skew,
|
||||||
|
data.path_taper_x,
|
||||||
|
data.path_taper_y,
|
||||||
|
data.path_twist_begin,
|
||||||
|
data.path_twist,
|
||||||
|
];
|
||||||
|
if !parameters.into_iter().all(f32::is_finite) {
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"non-finite primitive construction data",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let (mut sides, mut profile_begin, mut profile_end, sphere_mode) =
|
||||||
|
match data.profile_curve_with_property() {
|
||||||
|
ProfileCurve::Circle => (lod_sides(lod), data.profile_begin, data.profile_end, false),
|
||||||
|
ProfileCurve::EqualTriangle
|
||||||
|
| ProfileCurve::IsoTriangle
|
||||||
|
| ProfileCurve::RightTriangle => (3, data.profile_begin, data.profile_end, false),
|
||||||
|
ProfileCurve::HalfCircle => (
|
||||||
|
lod_sides(lod),
|
||||||
|
data.profile_begin.mul_add(0.5, 0.5),
|
||||||
|
data.profile_end.mul_add(0.5, 0.5),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
ProfileCurve::Square => (4, data.profile_begin, data.profile_end, false),
|
||||||
|
};
|
||||||
|
if sides < 3 {
|
||||||
|
sides = 3;
|
||||||
|
}
|
||||||
|
profile_begin = profile_begin.clamp(0.0, 1.0);
|
||||||
|
profile_end = profile_end.clamp(0.0, 1.0);
|
||||||
|
let hollow_sides = if data.profile_hole() == HoleType::Circle {
|
||||||
|
lod_sides(lod)
|
||||||
|
} else if data.profile_hole() == HoleType::Triangle {
|
||||||
|
3
|
||||||
|
} else if data.profile_hole() == HoleType::Same {
|
||||||
|
sides
|
||||||
|
} else {
|
||||||
|
4
|
||||||
|
};
|
||||||
|
let mut mesh = PrimMesh::new(
|
||||||
|
sides,
|
||||||
|
profile_begin,
|
||||||
|
profile_end,
|
||||||
|
data.profile_hollow,
|
||||||
|
hollow_sides,
|
||||||
|
)
|
||||||
|
.map_err(|_| render_error(source, "initialize primitive profile"))?;
|
||||||
|
mesh.viewer_mode = true;
|
||||||
|
mesh.sphere_mode = sphere_mode;
|
||||||
|
mesh.hole_size_x = data.path_scale_x;
|
||||||
|
mesh.hole_size_y = data.path_scale_y;
|
||||||
|
mesh.path_cut_begin = data.path_begin;
|
||||||
|
mesh.path_cut_end = data.path_end;
|
||||||
|
mesh.top_shear_x = data.path_shear_x;
|
||||||
|
mesh.top_shear_y = data.path_shear_y;
|
||||||
|
mesh.radius = data.path_radius_offset;
|
||||||
|
mesh.revolutions = data.path_revolutions;
|
||||||
|
mesh.skew = data.path_skew;
|
||||||
|
mesh.steps_per_revolution = lod_sides(lod);
|
||||||
|
let path_type = if matches!(data.path_curve, PathCurve::Line | PathCurve::Flexible) {
|
||||||
|
mesh.taper_x = 1.0 - data.path_scale_x;
|
||||||
|
mesh.taper_y = 1.0 - data.path_scale_y;
|
||||||
|
mesh.twist_begin = checked_twist(data.path_twist_begin, 180.0, source)?;
|
||||||
|
mesh.twist_end = checked_twist(data.path_twist, 180.0, source)?;
|
||||||
|
PathType::Linear
|
||||||
|
} else {
|
||||||
|
mesh.taper_x = data.path_taper_x;
|
||||||
|
mesh.taper_y = data.path_taper_y;
|
||||||
|
mesh.twist_begin = checked_twist(data.path_twist_begin, 360.0, source)?;
|
||||||
|
mesh.twist_end = checked_twist(data.path_twist, 360.0, source)?;
|
||||||
|
PathType::Circular
|
||||||
|
};
|
||||||
|
mesh.extrude(path_type)
|
||||||
|
.map_err(|_| render_error(source, "extrude primitive geometry"))?;
|
||||||
|
if mesh.coords.len() > MAX_TOTAL_VERTICES || mesh.viewer_faces.len() > MAX_TOTAL_INDICES / 3 {
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"primitive geometry exceeds renderer bounds",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indexed_faces(
|
||||||
|
prim: &Primitive,
|
||||||
|
indexer: VertexIndexer,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<Vec<Face>, Error> {
|
||||||
|
let count = usize::try_from(indexer.num_prim_faces)
|
||||||
|
.map_err(|_| render_error(source, "invalid primitive face count"))?;
|
||||||
|
if count != indexer.viewer_vertices.len() || count != indexer.viewer_polygons.len() {
|
||||||
|
return Err(render_error(source, "misaligned primitive face domains"));
|
||||||
|
}
|
||||||
|
let mut total_vertices = 0usize;
|
||||||
|
let mut total_indices = 0usize;
|
||||||
|
let mut faces = Vec::with_capacity(count);
|
||||||
|
for face_index in 0..count {
|
||||||
|
let source_vertices = &indexer.viewer_vertices[face_index];
|
||||||
|
let polygons = indexer.viewer_polygons[face_index]
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| render_error(source, "primitive face has no polygon domain"))?;
|
||||||
|
if source_vertices.is_empty() {
|
||||||
|
if polygons.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"primitive face polygons have no vertex domain",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if source_vertices.len() > MAX_FACE_VERTICES {
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"primitive face exceeds 16-bit vertex range",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
total_vertices = total_vertices
|
||||||
|
.checked_add(source_vertices.len())
|
||||||
|
.ok_or_else(|| render_error(source, "primitive vertex count overflow"))?;
|
||||||
|
if total_vertices > MAX_TOTAL_VERTICES {
|
||||||
|
return Err(render_error(source, "primitive vertex budget exceeded"));
|
||||||
|
}
|
||||||
|
let vertices = source_vertices
|
||||||
|
.iter()
|
||||||
|
.map(|vertex| {
|
||||||
|
checked_vertex(
|
||||||
|
Vector3 {
|
||||||
|
x: vertex.v.x,
|
||||||
|
y: vertex.v.y,
|
||||||
|
z: vertex.v.z,
|
||||||
|
},
|
||||||
|
Vector3 {
|
||||||
|
x: vertex.n.x,
|
||||||
|
y: vertex.n.y,
|
||||||
|
z: vertex.n.z,
|
||||||
|
},
|
||||||
|
Vector2 {
|
||||||
|
x: vertex.uv.u,
|
||||||
|
y: 1.0 - vertex.uv.v,
|
||||||
|
},
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let mut indices = Vec::with_capacity(
|
||||||
|
polygons
|
||||||
|
.len()
|
||||||
|
.checked_mul(3)
|
||||||
|
.ok_or_else(|| render_error(source, "primitive index count overflow"))?,
|
||||||
|
);
|
||||||
|
for polygon in polygons {
|
||||||
|
if polygon.v1 == polygon.v2 || polygon.v1 == polygon.v3 || polygon.v2 == polygon.v3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push_triangle(
|
||||||
|
&mut indices,
|
||||||
|
polygon.v1,
|
||||||
|
polygon.v2,
|
||||||
|
polygon.v3,
|
||||||
|
vertices.len(),
|
||||||
|
source,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
total_indices = total_indices
|
||||||
|
.checked_add(indices.len())
|
||||||
|
.ok_or_else(|| render_error(source, "primitive index count overflow"))?;
|
||||||
|
if total_indices > MAX_TOTAL_INDICES {
|
||||||
|
return Err(render_error(source, "primitive index budget exceeded"));
|
||||||
|
}
|
||||||
|
let texture = texture_for_face(prim, face_index, source)?;
|
||||||
|
faces.push(make_face(
|
||||||
|
i32::try_from(face_index)
|
||||||
|
.map_err(|_| render_error(source, "primitive face id overflow"))?,
|
||||||
|
vertices,
|
||||||
|
indices,
|
||||||
|
texture,
|
||||||
|
source,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
Ok(faces)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_faces(
|
||||||
|
faces: Vec<Face>,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<SimpleMesh, Error> {
|
||||||
|
let mut vertices = Vec::new();
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
for face in faces {
|
||||||
|
let base = vertices.len();
|
||||||
|
let combined = base
|
||||||
|
.checked_add(face.vertices.len())
|
||||||
|
.ok_or_else(|| render_error(source, "simple mesh vertex count overflow"))?;
|
||||||
|
if combined > MAX_TOTAL_VERTICES {
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"simple mesh exceeds 16-bit vertex range",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for index in face.indices {
|
||||||
|
let global = base
|
||||||
|
.checked_add(usize::from(index))
|
||||||
|
.ok_or_else(|| render_error(source, "simple mesh index overflow"))?;
|
||||||
|
if global >= combined {
|
||||||
|
return Err(render_error(source, "simple mesh index is out of range"));
|
||||||
|
}
|
||||||
|
indices.push(
|
||||||
|
u16::try_from(global)
|
||||||
|
.map_err(|_| render_error(source, "simple mesh index exceeds 16 bits"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vertices.extend(face.vertices);
|
||||||
|
}
|
||||||
|
if indices.len() > MAX_TOTAL_INDICES {
|
||||||
|
return Err(render_error(source, "simple mesh index budget exceeded"));
|
||||||
|
}
|
||||||
|
Ok(SimpleMesh { indices, vertices })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_for_face(
|
||||||
|
prim: &Primitive,
|
||||||
|
index: usize,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<PrimitiveTextureEntryFace, Error> {
|
||||||
|
let Some(textures) = prim.textures.as_ref() else {
|
||||||
|
return Ok(PrimitiveTextureEntryFace::default());
|
||||||
|
};
|
||||||
|
let index =
|
||||||
|
u32::try_from(index).map_err(|_| render_error(source, "texture face index overflow"))?;
|
||||||
|
textures
|
||||||
|
.get_face(index)
|
||||||
|
.map_err(|_| render_error(source, "texture face index is out of range"))
|
||||||
|
.map(|face| face.cloned().unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_face(
|
||||||
|
id: i32,
|
||||||
|
vertices: Vec<Vertex>,
|
||||||
|
indices: Vec<u16>,
|
||||||
|
texture_face: PrimitiveTextureEntryFace,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<Face, Error> {
|
||||||
|
let first = vertices
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| render_error(source, "render face has no vertices"))?;
|
||||||
|
let mut min = first.position;
|
||||||
|
let mut max = first.position;
|
||||||
|
for vertex in &vertices[1..] {
|
||||||
|
min.x = min.x.min(vertex.position.x);
|
||||||
|
min.y = min.y.min(vertex.position.y);
|
||||||
|
min.z = min.z.min(vertex.position.z);
|
||||||
|
max.x = max.x.max(vertex.position.x);
|
||||||
|
max.y = max.y.max(vertex.position.y);
|
||||||
|
max.z = max.z.max(vertex.position.z);
|
||||||
|
}
|
||||||
|
let center = Vector3 {
|
||||||
|
x: (min.x + max.x) * 0.5,
|
||||||
|
y: (min.y + max.y) * 0.5,
|
||||||
|
z: (min.z + max.z) * 0.5,
|
||||||
|
};
|
||||||
|
Ok(Face {
|
||||||
|
begin_s: 0,
|
||||||
|
begin_t: 0,
|
||||||
|
center,
|
||||||
|
edge: Vec::new(),
|
||||||
|
id,
|
||||||
|
indices,
|
||||||
|
mask: FaceMask::SINGLE,
|
||||||
|
max_extent: max,
|
||||||
|
min_extent: min,
|
||||||
|
normalized_scale: Vector3 {
|
||||||
|
x: max.x - min.x,
|
||||||
|
y: max.y - min.y,
|
||||||
|
z: max.z - min.z,
|
||||||
|
},
|
||||||
|
num_s: 0,
|
||||||
|
num_t: 0,
|
||||||
|
tex_coords1: None,
|
||||||
|
texture_face,
|
||||||
|
user_data: Object::Undefined,
|
||||||
|
vertices,
|
||||||
|
weights: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_vertex(
|
||||||
|
position: Vector3,
|
||||||
|
normal: Vector3,
|
||||||
|
tex_coord: Vector2,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<Vertex, Error> {
|
||||||
|
if !finite3(position) || !finite3(normal) || !finite2(tex_coord) {
|
||||||
|
return Err(render_error(
|
||||||
|
source,
|
||||||
|
"renderer produced a non-finite vertex",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Vertex {
|
||||||
|
position,
|
||||||
|
normal,
|
||||||
|
tex_coord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_triangle(
|
||||||
|
indices: &mut Vec<u16>,
|
||||||
|
v1: i32,
|
||||||
|
v2: i32,
|
||||||
|
v3: i32,
|
||||||
|
vertex_count: usize,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
for index in [v1, v2, v3] {
|
||||||
|
let index =
|
||||||
|
usize::try_from(index).map_err(|_| render_error(source, "negative renderer index"))?;
|
||||||
|
if index >= vertex_count {
|
||||||
|
return Err(render_error(source, "renderer index is out of range"));
|
||||||
|
}
|
||||||
|
indices.push(
|
||||||
|
u16::try_from(index)
|
||||||
|
.map_err(|_| render_error(source, "renderer index exceeds 16 bits"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_twist(
|
||||||
|
value: f32,
|
||||||
|
multiplier: f32,
|
||||||
|
source: libremetaverse_types::UUID,
|
||||||
|
) -> Result<i32, Error> {
|
||||||
|
let degrees = value * multiplier;
|
||||||
|
if !degrees.is_finite() || degrees.abs() > 360_000.0 {
|
||||||
|
return Err(render_error(source, "primitive twist is out of range"));
|
||||||
|
}
|
||||||
|
Ok(degrees as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn lod_sides(lod: DetailLevel) -> i32 {
|
||||||
|
match lod {
|
||||||
|
DetailLevel::Low => 6,
|
||||||
|
DetailLevel::Medium => 12,
|
||||||
|
DetailLevel::High | DetailLevel::Highest => 24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
|
||||||
|
Error::Rendering { source, context }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn finite2(value: Vector2) -> bool {
|
||||||
|
value.x.is_finite() && value.y.is_finite()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn finite3(value: Vector3) -> bool {
|
||||||
|
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn dot(left: Vector3, right: Vector3) -> f32 {
|
||||||
|
left.x * right.x + left.y * right.y + left.z * right.z
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn cross(left: Vector3, right: Vector3) -> Vector3 {
|
||||||
|
Vector3 {
|
||||||
|
x: left.y * right.z - left.z * right.y,
|
||||||
|
y: left.z * right.x - left.x * right.z,
|
||||||
|
z: left.x * right.y - left.y * right.x,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
use libremetaverse::rendering::{DetailLevel, SimpleMesh};
|
||||||
|
use libremetaverse::{
|
||||||
|
Primitive, PrimitiveConstructionData, PrimitiveSculptData, PrimitiveTextureEntry,
|
||||||
|
};
|
||||||
|
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||||
|
use libremetaverse_rendering_simple::SimpleRenderer;
|
||||||
|
use libremetaverse_types::{HoleType, PCode, PathCurve, ProfileCurve, SculptType, UUID};
|
||||||
|
|
||||||
|
fn primitive(profile: ProfileCurve, path: PathCurve) -> Primitive {
|
||||||
|
let mut primitive = Primitive::new_with_constructor().expect("Primitive constructor");
|
||||||
|
let mut data =
|
||||||
|
PrimitiveConstructionData::new_with_constructor().expect("ConstructionData constructor");
|
||||||
|
data.p_code = PCode::Prim;
|
||||||
|
data.path_curve = path;
|
||||||
|
data.set_profile_curve_with_property(profile);
|
||||||
|
data.path_scale_x = 0.8;
|
||||||
|
data.path_scale_y = 0.7;
|
||||||
|
data.path_begin = 0.05;
|
||||||
|
data.path_end = 0.95;
|
||||||
|
data.profile_begin = 0.03;
|
||||||
|
data.profile_end = 0.97;
|
||||||
|
data.profile_hollow = 0.15;
|
||||||
|
data.set_profile_hole(HoleType::Circle);
|
||||||
|
data.path_shear_x = 0.1;
|
||||||
|
data.path_shear_y = -0.1;
|
||||||
|
data.path_revolutions = 1.25;
|
||||||
|
data.path_twist = 0.1;
|
||||||
|
primitive.prim_data = data;
|
||||||
|
primitive.textures = Some(
|
||||||
|
PrimitiveTextureEntry::new_with_uuid(UUID::zero()).expect("texture entry constructor"),
|
||||||
|
);
|
||||||
|
primitive
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_valid(mesh: &SimpleMesh) {
|
||||||
|
assert!(!mesh.vertices.is_empty());
|
||||||
|
assert!(!mesh.indices.is_empty());
|
||||||
|
assert_eq!(mesh.indices.len() % 3, 0);
|
||||||
|
for index in &mesh.indices {
|
||||||
|
assert!(usize::from(*index) < mesh.vertices.len());
|
||||||
|
}
|
||||||
|
for vertex in &mesh.vertices {
|
||||||
|
assert!(vertex.position.x.is_finite());
|
||||||
|
assert!(vertex.position.y.is_finite());
|
||||||
|
assert!(vertex.position.z.is_finite());
|
||||||
|
assert!(vertex.normal.x.is_finite());
|
||||||
|
assert!(vertex.normal.y.is_finite());
|
||||||
|
assert!(vertex.normal.z.is_finite());
|
||||||
|
assert!(vertex.tex_coord.x.is_finite());
|
||||||
|
assert!(vertex.tex_coord.y.is_finite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sculpt_image() -> ManagedImage {
|
||||||
|
let mut image = ManagedImage::new(32, 32, ManagedImageImageChannels::COLOR)
|
||||||
|
.expect("sculpt image constructor");
|
||||||
|
for (index, red) in image.red.iter_mut().enumerate() {
|
||||||
|
*red = u8::try_from(index % 256).expect("bounded sample");
|
||||||
|
image.green[index] = u8::try_from((index * 3) % 256).expect("bounded sample");
|
||||||
|
image.blue[index] = u8::try_from((index * 7) % 256).expect("bounded sample");
|
||||||
|
}
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_profile_path_and_lod_produces_checked_geometry() {
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
for profile in [
|
||||||
|
ProfileCurve::Circle,
|
||||||
|
ProfileCurve::EqualTriangle,
|
||||||
|
ProfileCurve::HalfCircle,
|
||||||
|
ProfileCurve::IsoTriangle,
|
||||||
|
ProfileCurve::RightTriangle,
|
||||||
|
ProfileCurve::Square,
|
||||||
|
] {
|
||||||
|
for path in [PathCurve::Flexible, PathCurve::Line, PathCurve::Circle] {
|
||||||
|
for lod in [
|
||||||
|
DetailLevel::Low,
|
||||||
|
DetailLevel::Medium,
|
||||||
|
DetailLevel::High,
|
||||||
|
DetailLevel::Highest,
|
||||||
|
] {
|
||||||
|
let mesh = renderer
|
||||||
|
.generate_simple_mesh(primitive(profile, path), lod)
|
||||||
|
.expect("supported prim topology");
|
||||||
|
assert_valid(&mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_sculpt_topology_produces_checked_geometry_at_each_lod() {
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
for sculpt_type in [
|
||||||
|
SculptType::Plane,
|
||||||
|
SculptType::Cylinder,
|
||||||
|
SculptType::Sphere,
|
||||||
|
SculptType::Torus,
|
||||||
|
] {
|
||||||
|
for lod in [
|
||||||
|
DetailLevel::Low,
|
||||||
|
DetailLevel::Medium,
|
||||||
|
DetailLevel::High,
|
||||||
|
DetailLevel::Highest,
|
||||||
|
] {
|
||||||
|
let mut prim = primitive(ProfileCurve::Square, PathCurve::Line);
|
||||||
|
let mut sculpt = PrimitiveSculptData::new_with_constructor().expect("sculpt metadata");
|
||||||
|
sculpt.set_type_(sculpt_type);
|
||||||
|
prim.sculpt = Some(sculpt);
|
||||||
|
let mesh = renderer
|
||||||
|
.generate_simple_sculpt_mesh(prim, sculpt_image(), lod)
|
||||||
|
.expect("supported sculpt topology");
|
||||||
|
assert_valid(&mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,13 @@ pub enum Error {
|
|||||||
HttpRequest,
|
HttpRequest,
|
||||||
/// A host lookup or socket operation failed.
|
/// A host lookup or socket operation failed.
|
||||||
Socket,
|
Socket,
|
||||||
|
/// Rendering failed while processing one source object at a named stage.
|
||||||
|
Rendering {
|
||||||
|
/// UUID of the primitive whose geometry could not be produced.
|
||||||
|
source: crate::UUID,
|
||||||
|
/// Stable stage suitable for diagnostics and fixture assertions.
|
||||||
|
context: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
@@ -83,7 +90,8 @@ impl Error {
|
|||||||
| Self::Parse { .. }
|
| Self::Parse { .. }
|
||||||
| Self::Cancelled
|
| Self::Cancelled
|
||||||
| Self::HttpRequest
|
| Self::HttpRequest
|
||||||
| Self::Socket => None,
|
| Self::Socket
|
||||||
|
| Self::Rendering { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,6 +118,9 @@ impl fmt::Display for Error {
|
|||||||
Self::Cancelled => formatter.write_str("operation was cancelled"),
|
Self::Cancelled => formatter.write_str("operation was cancelled"),
|
||||||
Self::HttpRequest => formatter.write_str("HTTP request failed"),
|
Self::HttpRequest => formatter.write_str("HTTP request failed"),
|
||||||
Self::Socket => formatter.write_str("socket operation failed"),
|
Self::Socket => formatter.write_str("socket operation failed"),
|
||||||
|
Self::Rendering { source, context } => {
|
||||||
|
write!(formatter, "rendering object {source} failed: {context}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ Milestone 06 owns the native implementations in `libremetaverse-imaging`,
|
|||||||
`libremetaverse-imaging-skia`, and `libremetaverse-prim-mesher`. The core
|
`libremetaverse-imaging-skia`, and `libremetaverse-prim-mesher`. The core
|
||||||
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: this boundary does not hide or
|
mesh data but remain a separate milestone: `SimpleRenderer` now provides the
|
||||||
reclassify their future `MeshFoundry` and `SimpleRenderer` work.
|
reference prim/sculpt conversion pipeline, while mesh-asset decoding and
|
||||||
|
rigging remain owned by the separate `MeshFoundry` issue. See the
|
||||||
|
[`SimpleRenderer` guide](../crates/libremetaverse-rendering-simple/README.md)
|
||||||
|
for that boundary.
|
||||||
|
|
||||||
## Feature boundary
|
## Feature boundary
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,115 @@
|
|||||||
use libremetaverse::Primitive;
|
#![allow(clippy::float_cmp, clippy::too_many_lines)]
|
||||||
use libremetaverse::rendering::{DetailLevel, SimpleMesh};
|
|
||||||
|
use libremetaverse::rendering::{DetailLevel, IRendering, SimpleMesh, Vertex};
|
||||||
|
use libremetaverse::{
|
||||||
|
MappingType, Primitive, PrimitiveConstructionData, PrimitiveSculptData, PrimitiveTextureEntry,
|
||||||
|
PrimitiveTextureEntryFace,
|
||||||
|
};
|
||||||
|
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||||
use libremetaverse_prim_mesher::PrimMesh;
|
use libremetaverse_prim_mesher::PrimMesh;
|
||||||
use libremetaverse_rendering_mesh_foundry::MeshFoundry;
|
use libremetaverse_rendering_mesh_foundry::MeshFoundry;
|
||||||
use libremetaverse_rendering_simple::SimpleRenderer;
|
use libremetaverse_rendering_simple::SimpleRenderer;
|
||||||
|
use libremetaverse_types::{
|
||||||
|
Error, HoleType, PCode, PathCurve, ProfileCurve, SculptType, UUID, Vector2, Vector3,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn uuid(value: &str) -> UUID {
|
||||||
|
UUID::new_with_string(value.into()).expect("fixture UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primitive(profile: ProfileCurve, path: PathCurve, path_scale_y: f32) -> Primitive {
|
||||||
|
let mut primitive = Primitive::new_with_constructor().expect("Primitive constructor");
|
||||||
|
let mut data =
|
||||||
|
PrimitiveConstructionData::new_with_constructor().expect("ConstructionData constructor");
|
||||||
|
data.p_code = PCode::Prim;
|
||||||
|
data.path_curve = path;
|
||||||
|
data.set_profile_curve_with_property(profile);
|
||||||
|
data.path_scale_x = 1.0;
|
||||||
|
data.path_scale_y = path_scale_y;
|
||||||
|
data.path_begin = 0.0;
|
||||||
|
data.path_end = 1.0;
|
||||||
|
data.profile_begin = 0.0;
|
||||||
|
data.profile_end = 1.0;
|
||||||
|
data.profile_hollow = 0.0;
|
||||||
|
data.set_profile_hole(HoleType::Same);
|
||||||
|
data.path_revolutions = 1.0;
|
||||||
|
primitive.prim_data = data;
|
||||||
|
primitive
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_primitive() -> Primitive {
|
||||||
|
primitive(ProfileCurve::Square, PathCurve::Line, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mesh_hash(mesh: &SimpleMesh) -> u64 {
|
||||||
|
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
|
||||||
|
let mut mix = |value: u32| {
|
||||||
|
hash ^= u64::from(value);
|
||||||
|
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
||||||
|
};
|
||||||
|
mix(u32::try_from(mesh.vertices.len()).expect("fixture vertex count"));
|
||||||
|
mix(u32::try_from(mesh.indices.len()).expect("fixture index count"));
|
||||||
|
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,
|
||||||
|
] {
|
||||||
|
mix(value.to_bits());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index in &mesh.indices {
|
||||||
|
mix(u32::from(*index));
|
||||||
|
}
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_valid_mesh(mesh: &SimpleMesh) {
|
||||||
|
assert!(!mesh.vertices.is_empty());
|
||||||
|
assert!(!mesh.indices.is_empty());
|
||||||
|
assert_eq!(mesh.indices.len() % 3, 0);
|
||||||
|
for index in &mesh.indices {
|
||||||
|
assert!(usize::from(*index) < mesh.vertices.len());
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
] {
|
||||||
|
assert!(value.is_finite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sculpt_image() -> ManagedImage {
|
||||||
|
let mut image = ManagedImage::new(8, 8, ManagedImageImageChannels::COLOR)
|
||||||
|
.expect("sculpt image constructor");
|
||||||
|
for index in 0_u8..64 {
|
||||||
|
image.red[usize::from(index)] = index.wrapping_mul(3);
|
||||||
|
image.green[usize::from(index)] = index.wrapping_mul(5);
|
||||||
|
image.blue[usize::from(index)] = index.wrapping_mul(7);
|
||||||
|
}
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertex(x: f32, y: f32, normal: Vector3, position: Vector3) -> Vertex {
|
||||||
|
Vertex {
|
||||||
|
position,
|
||||||
|
normal,
|
||||||
|
tex_coord: Vector2 { x, y },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mesh_and_renderer_calls_compile_with_typed_elements() {
|
fn mesh_and_renderer_calls_compile_with_typed_elements() {
|
||||||
@@ -23,17 +130,205 @@ fn mesh_and_renderer_calls_compile_with_typed_elements() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_mesher_and_remaining_shims_report_their_current_status() {
|
fn simple_renderer_box_pipeline_is_deterministic_and_matches_golden_mesh() {
|
||||||
let mesh = PrimMesh::new(4, 0.0, 1.0, 0.0, 4)
|
let renderer = SimpleRenderer::new().expect("SimpleRenderer constructor");
|
||||||
.expect("native PrimMesh constructor should create a checked mesh");
|
let faceted = renderer
|
||||||
assert!(mesh.coords.is_empty());
|
.generate_faceted_mesh(box_primitive(), DetailLevel::High)
|
||||||
assert!(mesh.faces.is_empty());
|
.expect("faceted box");
|
||||||
|
assert_eq!(faceted.faces.len(), 6);
|
||||||
let Err(renderer_error) = SimpleRenderer::new() else {
|
|
||||||
panic!("failure-only renderer constructor unexpectedly returned a value");
|
|
||||||
};
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
renderer_error.csharp_member(),
|
faceted
|
||||||
Some("M:LibreMetaverse.Rendering.SimpleRenderer.#ctor")
|
.faces
|
||||||
|
.iter()
|
||||||
|
.map(|face| face.vertices.len())
|
||||||
|
.sum::<usize>(),
|
||||||
|
24
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
faceted
|
||||||
|
.faces
|
||||||
|
.iter()
|
||||||
|
.map(|face| face.indices.len())
|
||||||
|
.sum::<usize>(),
|
||||||
|
36
|
||||||
|
);
|
||||||
|
for face in &faceted.faces {
|
||||||
|
assert!(face.min_extent.x <= face.max_extent.x);
|
||||||
|
assert!(face.min_extent.y <= face.max_extent.y);
|
||||||
|
assert!(face.min_extent.z <= face.max_extent.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = renderer
|
||||||
|
.generate_simple_mesh(box_primitive(), DetailLevel::High)
|
||||||
|
.expect("first simple box");
|
||||||
|
let second = renderer
|
||||||
|
.generate_simple_mesh(box_primitive(), DetailLevel::High)
|
||||||
|
.expect("second simple box");
|
||||||
|
assert_valid_mesh(&first);
|
||||||
|
assert_eq!(first.vertices.len(), 24);
|
||||||
|
assert_eq!(first.indices.len(), 36);
|
||||||
|
assert_eq!(mesh_hash(&first), mesh_hash(&second));
|
||||||
|
assert_eq!(mesh_hash(&first), 0x3d2b_10e3_7da0_3d2e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_renderer_maps_default_and_per_face_texture_metadata() {
|
||||||
|
let default_texture = uuid("11111111-1111-1111-1111-111111111111");
|
||||||
|
let override_texture = uuid("22222222-2222-2222-2222-222222222222");
|
||||||
|
let material = uuid("33333333-3333-3333-3333-333333333333");
|
||||||
|
let render_material = uuid("44444444-4444-4444-4444-444444444444");
|
||||||
|
let mut textures =
|
||||||
|
PrimitiveTextureEntry::new_with_uuid(default_texture).expect("texture entry constructor");
|
||||||
|
let face = textures.create_face(0).expect("face override");
|
||||||
|
face.set_texture_id(override_texture);
|
||||||
|
face.set_material_id(material);
|
||||||
|
face.set_render_material_id(render_material);
|
||||||
|
let mut prim = box_primitive();
|
||||||
|
prim.textures = Some(textures);
|
||||||
|
|
||||||
|
let mesh = SimpleRenderer::new()
|
||||||
|
.expect("renderer")
|
||||||
|
.generate_faceted_mesh(prim, DetailLevel::Medium)
|
||||||
|
.expect("textured box");
|
||||||
|
assert_eq!(mesh.faces[0].texture_face.texture_id(), override_texture);
|
||||||
|
assert_eq!(mesh.faces[0].texture_face.material_id(), material);
|
||||||
|
assert_eq!(
|
||||||
|
mesh.faces[0].texture_face.render_material_id(),
|
||||||
|
render_material
|
||||||
|
);
|
||||||
|
assert_eq!(mesh.faces[1].texture_face.texture_id(), default_texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_renderer_transforms_default_and_planar_texture_coordinates() {
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
let mut face = PrimitiveTextureEntryFace::new(None).expect("texture face");
|
||||||
|
face.set_repeat_u(2.0);
|
||||||
|
face.set_repeat_v(3.0);
|
||||||
|
face.set_offset_u(0.1);
|
||||||
|
face.set_offset_v(-0.2);
|
||||||
|
face.set_rotation(0.0);
|
||||||
|
face.set_tex_map_type(MappingType::Default);
|
||||||
|
let mut vertices = vec![vertex(0.25, 0.75, Vector3::unit_z(), Vector3::zero())];
|
||||||
|
renderer
|
||||||
|
.transform_tex_coords(
|
||||||
|
&mut vertices,
|
||||||
|
Vector3::zero(),
|
||||||
|
face,
|
||||||
|
Vector3 {
|
||||||
|
x: 1.0,
|
||||||
|
y: 1.0,
|
||||||
|
z: 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("default texture transform");
|
||||||
|
assert!((vertices[0].tex_coord.x - 0.1).abs() < 1.0e-6);
|
||||||
|
assert!((vertices[0].tex_coord.y - 1.05).abs() < 1.0e-6);
|
||||||
|
|
||||||
|
let mut planar = PrimitiveTextureEntryFace::new(None).expect("planar face");
|
||||||
|
planar.set_tex_map_type(MappingType::Planar);
|
||||||
|
let mut vertices = vec![vertex(
|
||||||
|
99.0,
|
||||||
|
99.0,
|
||||||
|
Vector3::unit_z(),
|
||||||
|
Vector3 {
|
||||||
|
x: 0.25,
|
||||||
|
y: -0.25,
|
||||||
|
z: 0.5,
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
renderer
|
||||||
|
.transform_tex_coords(
|
||||||
|
&mut vertices,
|
||||||
|
Vector3::zero(),
|
||||||
|
planar,
|
||||||
|
Vector3 {
|
||||||
|
x: 2.0,
|
||||||
|
y: 4.0,
|
||||||
|
z: 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("planar texture transform");
|
||||||
|
assert!((vertices[0].tex_coord.x - 1.5).abs() < 1.0e-6);
|
||||||
|
assert!((vertices[0].tex_coord.y - -1.5).abs() < 1.0e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_renderer_generates_deterministic_sculpt_geometry() {
|
||||||
|
let mut prim = box_primitive();
|
||||||
|
let mut sculpt = PrimitiveSculptData::new_with_constructor().expect("sculpt metadata");
|
||||||
|
sculpt.set_type_(SculptType::Plane);
|
||||||
|
prim.sculpt = Some(sculpt);
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
let first = renderer
|
||||||
|
.generate_simple_sculpt_mesh(prim.clone(), sculpt_image(), DetailLevel::Low)
|
||||||
|
.expect("first sculpt mesh");
|
||||||
|
let second = renderer
|
||||||
|
.generate_simple_sculpt_mesh(prim, sculpt_image(), DetailLevel::Low)
|
||||||
|
.expect("second sculpt mesh");
|
||||||
|
assert_valid_mesh(&first);
|
||||||
|
assert_eq!(mesh_hash(&first), mesh_hash(&second));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_renderer_trait_dispatch_returns_geometry() {
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
let mesh = IRendering::generate_simple_mesh(&renderer, box_primitive(), DetailLevel::Low)
|
||||||
|
.expect("trait call")
|
||||||
|
.expect("renderer returned geometry");
|
||||||
|
assert_valid_mesh(&mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_renderer_reports_source_context_for_invalid_and_mesh_sculpt_inputs() {
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
let source = uuid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
let mut invalid = box_primitive();
|
||||||
|
invalid.id = source;
|
||||||
|
invalid.prim_data.path_end = f32::NAN;
|
||||||
|
assert!(matches!(
|
||||||
|
renderer.generate_simple_mesh(invalid, DetailLevel::High),
|
||||||
|
Err(Error::Rendering {
|
||||||
|
source: actual,
|
||||||
|
context: "non-finite primitive construction data"
|
||||||
|
}) if actual == source
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut mesh_sculpt = box_primitive();
|
||||||
|
mesh_sculpt.id = source;
|
||||||
|
let mut sculpt = PrimitiveSculptData::new_with_constructor().expect("sculpt metadata");
|
||||||
|
sculpt.set_type_(SculptType::Mesh);
|
||||||
|
mesh_sculpt.sculpt = Some(sculpt);
|
||||||
|
assert!(matches!(
|
||||||
|
renderer.generate_simple_sculpt_mesh(
|
||||||
|
mesh_sculpt,
|
||||||
|
sculpt_image(),
|
||||||
|
DetailLevel::High,
|
||||||
|
),
|
||||||
|
Err(Error::Rendering {
|
||||||
|
source: actual,
|
||||||
|
context: "mesh sculpt requires a decoded mesh asset"
|
||||||
|
}) if actual == source
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_renderer_rejects_non_finite_texture_inputs() {
|
||||||
|
let renderer = SimpleRenderer::new().expect("renderer");
|
||||||
|
let mut face = PrimitiveTextureEntryFace::new(None).expect("texture face");
|
||||||
|
face.set_repeat_u(f32::INFINITY);
|
||||||
|
let mut vertices = vec![vertex(0.0, 0.0, Vector3::unit_z(), Vector3::zero())];
|
||||||
|
assert_eq!(
|
||||||
|
renderer.transform_tex_coords(
|
||||||
|
&mut vertices,
|
||||||
|
Vector3::zero(),
|
||||||
|
face,
|
||||||
|
Vector3 {
|
||||||
|
x: 1.0,
|
||||||
|
y: 1.0,
|
||||||
|
z: 1.0
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Err(Error::Argument)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
96
tools/check_milestone_10_issue_76.py
Normal file
96
tools/check_milestone_10_issue_76.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit issue 76's native SimpleRenderer 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-simple" / "src" / "simple_renderer.rs"
|
||||||
|
GENERATED = ROOT / "crates" / "libremetaverse-rendering-simple" / "src" / "generated.rs"
|
||||||
|
MANIFEST = ROOT / "crates" / "libremetaverse-rendering-simple" / "Cargo.toml"
|
||||||
|
COMPAT = ROOT / "tests" / "compat" / "tests" / "rendering_shims.rs"
|
||||||
|
REFERENCE = (
|
||||||
|
ROOT
|
||||||
|
/ "crates"
|
||||||
|
/ "libremetaverse-rendering-simple"
|
||||||
|
/ "tests"
|
||||||
|
/ "reference_pipeline.rs"
|
||||||
|
)
|
||||||
|
DOC = ROOT / "crates" / "libremetaverse-rendering-simple" / "README.md"
|
||||||
|
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.SimpleRenderer"
|
||||||
|
MEMBERS = {
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.#ctor",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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::simple_renderer::SimpleRenderer"
|
||||||
|
):
|
||||||
|
raise SystemExit("issue 76 SimpleRenderer native declaration is missing")
|
||||||
|
missing = sorted(MEMBERS - set(generate_api_shims.NATIVE_MEMBER_BODIES))
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("issue 76 native members missing: " + ", ".join(missing))
|
||||||
|
for source in (SOURCE, GENERATED):
|
||||||
|
if STUB_RE.search(source.read_text()):
|
||||||
|
raise SystemExit(f"issue 76 owned Rust stubs remain in {source.name}")
|
||||||
|
|
||||||
|
require_markers(SOURCE, (
|
||||||
|
"impl IRendering for SimpleRenderer", "PrimMesh", "SculptMesh",
|
||||||
|
"MAX_FACE_VERTICES", "checked_add", "u16::try_from", "checked_vertex",
|
||||||
|
"texture_for_face", "MappingType::Planar", "Error::Rendering",
|
||||||
|
"mesh sculpt requires a decoded mesh asset",
|
||||||
|
))
|
||||||
|
manifest = MANIFEST.read_text()
|
||||||
|
if "libremetaverse-prim-mesher" not in manifest:
|
||||||
|
raise SystemExit("issue 76 must depend on the checked PrimMesher crate")
|
||||||
|
forbidden = ("skia", "voice", "libremetaverse-rlv", "libremetaverse-lsl")
|
||||||
|
leaked = [name for name in forbidden if name in manifest.lower()]
|
||||||
|
if leaked:
|
||||||
|
raise SystemExit("issue 76 dependency boundary leaked: " + ", ".join(leaked))
|
||||||
|
|
||||||
|
require_markers(COMPAT, (
|
||||||
|
"matches_golden_mesh", "0x3d2b_10e3_7da0_3d2e",
|
||||||
|
"maps_default_and_per_face_texture_metadata", "transforms_default_and_planar",
|
||||||
|
"generates_deterministic_sculpt_geometry", "trait_dispatch_returns_geometry",
|
||||||
|
"reports_source_context", "rejects_non_finite_texture_inputs",
|
||||||
|
))
|
||||||
|
require_markers(REFERENCE, (
|
||||||
|
"every_profile_path_and_lod_produces_checked_geometry",
|
||||||
|
"ProfileCurve::RightTriangle", "PathCurve::Flexible", "DetailLevel::Highest",
|
||||||
|
"every_sculpt_topology_produces_checked_geometry_at_each_lod",
|
||||||
|
"SculptType::Cylinder", "SculptType::Sphere", "SculptType::Torus",
|
||||||
|
))
|
||||||
|
require_markers(DOC, (
|
||||||
|
"all four `DetailLevel`", "65,536", "Error::Rendering",
|
||||||
|
"source primitive UUID", "MeshFoundry", "prim-local coordinates",
|
||||||
|
))
|
||||||
|
require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_76.py",))
|
||||||
|
print(
|
||||||
|
"issue 76 audit: native prim/sculpt conversion, face metadata, texture mapping, "
|
||||||
|
"bounded errors, deterministic evidence, dependency isolation, and docs are present"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -503,9 +503,21 @@ NATIVE_DECLARATIONS = {
|
|||||||
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
|
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
|
||||||
"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",
|
||||||
}
|
}
|
||||||
|
|
||||||
NATIVE_MEMBER_BODIES = {
|
NATIVE_MEMBER_BODIES = {
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.#ctor": "Self::native_new()",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_faceted_mesh(prim, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_simple_mesh(prim, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
|
||||||
|
"self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)",
|
||||||
|
"M:LibreMetaverse.Rendering.SimpleRenderer.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)",
|
||||||
"F:LibreMetaverse.Assets.ArchiveConstants.ASSET_TYPE_TO_EXTENSION":
|
"F:LibreMetaverse.Assets.ArchiveConstants.ASSET_TYPE_TO_EXTENSION":
|
||||||
"crate::asset_archive::asset_type_to_extension()",
|
"crate::asset_archive::asset_type_to_extension()",
|
||||||
"F:LibreMetaverse.Assets.ArchiveConstants.EXTENSION_TO_ASSET_TYPE":
|
"F:LibreMetaverse.Assets.ArchiveConstants.EXTENSION_TO_ASSET_TYPE":
|
||||||
|
|||||||
@@ -1029,6 +1029,7 @@ def validate_generated_shims() -> None:
|
|||||||
"crate::transfers::",
|
"crate::transfers::",
|
||||||
"crate::xml_codec::",
|
"crate::xml_codec::",
|
||||||
"stored_visual_params",
|
"stored_visual_params",
|
||||||
|
"Self::native_",
|
||||||
"self.native_",
|
"self.native_",
|
||||||
"left.native_equals(",
|
"left.native_equals(",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user