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

This commit is contained in:
2026-08-10 21:50:59 +00:00
parent fdda05b53f
commit 611db567a0
16 changed files with 1306 additions and 37 deletions

View File

@@ -6,11 +6,11 @@
#![allow(non_snake_case)]
/// C# type: `T:LibreMetaverse.Rendering.SimpleRenderer`.
pub struct SimpleRenderer;
pub use crate::simple_renderer::SimpleRenderer;
impl SimpleRenderer {
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.#ctor`.
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)`.
pub fn generate_faceted_mesh(
@@ -18,9 +18,7 @@ impl SimpleRenderer {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_mesh(prim, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_sculpt_mesh(
@@ -29,9 +27,7 @@ impl SimpleRenderer {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_mesh(
@@ -39,9 +35,7 @@ impl SimpleRenderer {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_mesh(prim, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_sculpt_mesh(
@@ -50,9 +44,7 @@ impl SimpleRenderer {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
}
/// 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(
@@ -62,9 +54,6 @@ impl SimpleRenderer {
te_face: libremetaverse::PrimitiveTextureEntryFace,
prim_scale: libremetaverse_types::Vector3,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"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)
}
}
impl libremetaverse::rendering::IRendering for SimpleRenderer {}

View File

@@ -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;
mod generated;
mod simple_renderer;
pub use generated::*;
pub use libremetaverse as core;

View 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,
}
}