//! 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 { Ok(Self) } pub(crate) fn native_generate_simple_mesh( &self, prim: Primitive, lod: DetailLevel, ) -> Result { 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 { 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 { 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 { 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::, _>>()?; 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, 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, Error> { self.native_generate_faceted_mesh(prim, lod).map(Some) } fn generate_faceted_sculpt_mesh( &self, prim: Primitive, sculpt_texture: ManagedImage, lod: DetailLevel, ) -> Result, Error> { self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod) .map(Some) } fn generate_simple_mesh( &self, prim: Primitive, lod: DetailLevel, ) -> Result, Error> { self.native_generate_simple_mesh(prim, lod).map(Some) } fn generate_simple_sculpt_mesh( &self, prim: Primitive, sculpt_texture: ManagedImage, lod: DetailLevel, ) -> Result, Error> { self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod) .map(Some) } fn transform_tex_coords( &self, vertices: &mut Vec, 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 { 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, 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::, _>>()?; 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, source: libremetaverse_types::UUID, ) -> Result { 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 { 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, indices: Vec, texture_face: PrimitiveTextureEntryFace, source: libremetaverse_types::UUID, ) -> Result { 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 { 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, 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 { 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, } }