Files
MetaCrate/crates/libremetaverse-rendering-mesh-foundry/src/mesh_foundry.rs
Chili Palmer 88408c9680
Some checks failed
Native code generation / deterministic (push) Failing after 1m58s
Imaging and meshing gate / native (push) Failing after 4m10s
JPEG 2000 feature / linux (push) Successful in 3m0s
Native Rust workspace compile / compile (push) Failing after 6m12s
Skia feature / linux (push) Has been cancelled
Implement MeshFoundry pipeline (#77)
2026-08-10 22:37:43 +00:00

1437 lines
46 KiB
Rust

//! Native `MeshFoundry` implementation and bounded Second Life mesh decoding.
#![allow(clippy::cast_possible_truncation)] // Quantized protocol values are checked first.
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the mapped API.
#![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)] // Mapped constructors are fallible.
#![allow(clippy::unused_self)] // IRendering operations are instance methods.
use std::collections::HashMap;
use std::io::Read as _;
use flate2::read::ZlibDecoder;
use libremetaverse::rendering::{
DetailLevel, Face, FaceMask, FacetedMesh, IRendering, MeshSkinData, SimpleMesh, Vertex,
VertexWeight,
};
use libremetaverse::{Primitive, PrimitiveTextureEntryFace};
use libremetaverse_imaging::ManagedImage;
use libremetaverse_prim_mesher::SculptMesh;
use libremetaverse_rendering_simple::SimpleRenderer;
use libremetaverse_structured_data::{OSD, OSDMap, deserialize_llsd_binary_prefix};
use libremetaverse_types::compat::Object;
use libremetaverse_types::{Error, Vector2, Vector3, Vector4};
const MAX_ASSET_BYTES: usize = 64 * 1024 * 1024;
const MAX_SECTION_BYTES: usize = 64 * 1024 * 1024;
const MAX_SECTIONS: usize = 256;
const MAX_SUBMESHES: usize = 256;
const MAX_FACE_VERTICES: usize = 65_536;
const MAX_TOTAL_VERTICES: usize = 1_000_000;
const MAX_TOTAL_INDICES: usize = 6_000_000;
const MAX_JOINTS: usize = 512;
const MAX_HULLS: usize = 65_536;
const MAX_HULL_VERTICES: usize = 1_000_000;
/// Mesh-only per-face domains that are not representable by the mapped `Face`
/// fields. Consumers can recover this value with `face.user_data.downcast_ref`.
#[derive(Debug)]
pub struct MeshFaceAux {
/// Tangent vectors in vertex order; `w` is bitangent handedness.
pub tangents: Vec<Vector4>,
}
/// Full native renderer for prims, sculpts, terrain, and LLSD mesh assets.
pub struct MeshFoundry;
impl MeshFoundry {
pub(crate) fn native_new() -> Result<Self, Error> {
Ok(Self)
}
pub(crate) fn native_generate_faceted_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<Option<FacetedMesh>, Error> {
SimpleRenderer::new()?
.generate_faceted_mesh(prim, lod)
.map(Some)
}
pub(crate) fn native_generate_simple_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
_with_normals: bool,
) -> Result<Option<SimpleMesh>, Error> {
// The native pipeline always retains normals and UVs. This is a strict
// improvement over the legacy non-normal overload and keeps one stable
// vertex contract for downstream consumers.
SimpleRenderer::new()?
.generate_simple_mesh(prim, lod)
.map(Some)
}
pub(crate) fn native_generate_faceted_sculpt_mesh(
&self,
prim: Primitive,
sculpt_texture: ManagedImage,
lod: DetailLevel,
) -> Result<Option<FacetedMesh>, Error> {
SimpleRenderer::new()?
.generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
.map(Some)
}
pub(crate) fn native_generate_simple_sculpt_mesh(
&self,
prim: Primitive,
sculpt_texture: ManagedImage,
lod: DetailLevel,
) -> Result<Option<SimpleMesh>, Error> {
SimpleRenderer::new()?
.generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
.map(Some)
}
pub(crate) fn native_transform_tex_coords(
&self,
vertices: &mut Vec<Vertex>,
center: Vector3,
te_face: PrimitiveTextureEntryFace,
prim_scale: Vector3,
) -> Result<(), Error> {
SimpleRenderer::new()?.transform_tex_coords(vertices, center, te_face, prim_scale)
}
pub(crate) fn native_generate_faceted_mesh_mesh(
&self,
prim: Primitive,
mesh_data: Vec<u8>,
requested_lod: DetailLevel,
) -> Result<Option<FacetedMesh>, Error> {
let source = prim.id;
let unpacked = unpack_mesh_map(&mesh_data, source)?;
let (lod, faces) = select_lod(&unpacked, requested_lod)
.ok_or_else(|| render_error(source, "mesh asset has no visual LOD"))?;
let skin = unpacked
.get("skin")
.map(|value| decode_skin(value, source))
.transpose()?;
let mut mesh = decode_faceted(&prim, faces, skin, lod, source)?;
apply_sculpt_modifiers(&prim, &mut mesh)?;
Ok(Some(mesh))
}
pub(crate) fn native_unpack_mesh(&self, asset_data: Vec<u8>) -> Result<OSDMap, Error> {
let decoded = unpack_mesh_map(&asset_data, libremetaverse_types::UUID::zero())?;
OSDMap::new_with_dictionary(decoded)
}
pub(crate) fn native_mesh_sub_mesh_as_simple_mesh(
&self,
prim: Primitive,
compressed_mesh_data: Vec<u8>,
) -> Result<Option<SimpleMesh>, Error> {
let source = prim.id;
let value = decompress_osd(&compressed_mesh_data, source)?;
let OSD::Array(submeshes) = value else {
return Err(render_error(source, "mesh section is not a submesh array"));
};
if submeshes.len() > MAX_SUBMESHES {
return Err(render_error(source, "mesh submesh count exceeds limit"));
}
let mut faces = Vec::new();
let mut total_vertices = 0usize;
let mut total_indices = 0usize;
for (index, submesh) in submeshes.iter().enumerate() {
if let Some(face) = decode_submesh(&prim, submesh, index, None, source)? {
total_vertices = checked_total(
total_vertices,
face.vertices.len(),
MAX_TOTAL_VERTICES,
source,
"mesh vertex budget exceeded",
)?;
total_indices = checked_total(
total_indices,
face.indices.len(),
MAX_TOTAL_INDICES,
source,
"mesh index budget exceeded",
)?;
faces.push(face);
}
}
flatten_faces(faces, source).map(Some)
}
pub(crate) fn native_mesh_sub_mesh_as_convex_hulls(
&self,
prim: Primitive,
compressed_mesh_data: Vec<u8>,
bounding_hull: Option<&mut Vec<Vector3>>,
) -> Result<Vec<Vec<Vector3>>, Error> {
let source = prim.id;
let value = decompress_osd(&compressed_mesh_data, source)?;
let map = as_map(&value)
.ok_or_else(|| render_error(source, "physics convex section is not a map"))?;
let min = map
.get("Min")
.map(|value| osd_vector3(value, source, "invalid convex minimum"))
.transpose()?
.unwrap_or(Vector3 {
x: -0.5,
y: -0.5,
z: -0.5,
});
let max = map
.get("Max")
.map(|value| osd_vector3(value, source, "invalid convex maximum"))
.transpose()?
.unwrap_or(Vector3 {
x: 0.5,
y: 0.5,
z: 0.5,
});
validate_domain3(min, max, source, "invalid convex position domain")?;
let decoded_bounding = map
.get("BoundingVerts")
.and_then(osd_binary)
.map(|bytes| decode_quantized_positions(bytes, min, max, source))
.transpose()?;
if let Some(output) = bounding_hull {
output.clear();
if let Some(vertices) = decoded_bounding {
*output = vertices;
}
}
let Some(counts) = map.get("HullList").and_then(osd_binary) else {
return Ok(Vec::new());
};
let positions = map
.get("Positions")
.and_then(osd_binary)
.ok_or_else(|| render_error(source, "convex hull positions are missing"))?;
if counts.len() > MAX_HULLS {
return Err(render_error(source, "convex hull count exceeds limit"));
}
let mut cursor = 0usize;
let mut total = 0usize;
let mut hulls = Vec::with_capacity(counts.len());
for encoded_count in counts {
let count = if *encoded_count == 0 {
256usize
} else {
usize::from(*encoded_count)
};
total = checked_total(
total,
count,
MAX_HULL_VERTICES,
source,
"convex hull vertex budget exceeded",
)?;
let byte_count = count
.checked_mul(6)
.ok_or_else(|| render_error(source, "convex hull byte count overflow"))?;
let end = cursor
.checked_add(byte_count)
.ok_or_else(|| render_error(source, "convex hull offset overflow"))?;
let bytes = positions
.get(cursor..end)
.ok_or_else(|| render_error(source, "truncated convex hull positions"))?;
hulls.push(decode_quantized_positions(bytes, min, max, source)?);
cursor = end;
}
if cursor != positions.len() {
return Err(render_error(source, "trailing convex hull positions"));
}
Ok(hulls)
}
pub(crate) fn native_terrain_mesh(
&self,
z_map: Vec<Vec<f32>>,
x_begin: f32,
x_end: f32,
y_begin: f32,
y_end: f32,
) -> Result<Face, Error> {
let source = libremetaverse_types::UUID::zero();
let mesh = SculptMesh::from_height_rows(z_map, x_begin, x_end, y_begin, y_end, true)
.map_err(|_| render_error(source, "generate bounded terrain 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 terrain 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, "terrain index count overflow"))?,
);
for face in mesh.faces {
push_triangle(
&mut indices,
face.v1,
face.v2,
face.v3,
vertices.len(),
source,
)?;
}
make_face(
0,
vertices,
indices,
PrimitiveTextureEntryFace::default(),
Object::Undefined,
Vector3::one(),
source,
)
}
}
impl IRendering for MeshFoundry {
fn generate_faceted_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<Option<FacetedMesh>, Error> {
self.native_generate_faceted_mesh(prim, lod)
}
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)
}
fn generate_simple_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<Option<SimpleMesh>, Error> {
self.native_generate_simple_mesh(prim, lod, true)
}
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)
}
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 unpack_mesh_map(
asset_data: &[u8],
source: libremetaverse_types::UUID,
) -> Result<HashMap<String, OSD>, Error> {
if asset_data.is_empty() || asset_data.len() > MAX_ASSET_BYTES {
return Err(render_error(source, "mesh asset size is out of range"));
}
let (header_value, payload_start) = deserialize_llsd_binary_prefix(asset_data)
.map_err(|_| render_error(source, "decode mesh asset header"))?;
let OSD::Map(header) = header_value else {
return Err(render_error(source, "mesh asset header is not a map"));
};
if header.len() > MAX_SECTIONS {
return Err(render_error(source, "mesh section count exceeds limit"));
}
let mut output = HashMap::with_capacity(header.len() + 1);
output.insert("asset_header".to_owned(), OSD::Map(header.clone()));
for (name, value) in header {
let OSD::Map(info) = &value else {
output.insert(name, value);
continue;
};
let (Some(offset), Some(size)) = (
info.get("offset").and_then(osd_integer),
info.get("size").and_then(osd_integer),
) else {
output.insert(name, value);
continue;
};
if offset < 0 || size == 0 {
output.insert(name, value);
continue;
}
let offset = usize::try_from(offset)
.map_err(|_| render_error(source, "mesh section offset is invalid"))?;
let size = usize::try_from(size)
.map_err(|_| render_error(source, "mesh section size is invalid"))?;
if size > MAX_SECTION_BYTES {
return Err(render_error(
source,
"compressed mesh section exceeds limit",
));
}
let begin = payload_start
.checked_add(offset)
.ok_or_else(|| render_error(source, "mesh section offset overflow"))?;
let end = begin
.checked_add(size)
.ok_or_else(|| render_error(source, "mesh section size overflow"))?;
let section = asset_data
.get(begin..end)
.ok_or_else(|| render_error(source, "mesh section is outside the asset"))?;
output.insert(name, decompress_osd(section, source)?);
}
Ok(output)
}
fn decompress_osd(compressed: &[u8], source: libremetaverse_types::UUID) -> Result<OSD, Error> {
if compressed.is_empty() || compressed.len() > MAX_SECTION_BYTES {
return Err(render_error(
source,
"compressed mesh section size is out of range",
));
}
let mut zlib = ZlibDecoder::new(compressed);
let mut output = Vec::new();
(&mut zlib)
.take((MAX_SECTION_BYTES + 1) as u64)
.read_to_end(&mut output)
.map_err(|_| render_error(source, "decompress mesh section"))?;
if output.len() > MAX_SECTION_BYTES {
return Err(render_error(
source,
"decompressed mesh section exceeds limit",
));
}
let (value, _) = deserialize_llsd_binary_prefix(&output)
.map_err(|_| render_error(source, "decode mesh section LLSD"))?;
Ok(value)
}
fn select_lod(
unpacked: &HashMap<String, OSD>,
requested: DetailLevel,
) -> Option<(DetailLevel, &[OSD])> {
let order = [
requested,
DetailLevel::Highest,
DetailLevel::High,
DetailLevel::Medium,
DetailLevel::Low,
];
for (index, candidate) in order.into_iter().enumerate() {
if index > 0 && candidate == requested {
continue;
}
let key = lod_key(candidate);
if let Some(OSD::Array(values)) = unpacked.get(key)
&& !values.is_empty()
{
return Some((candidate, values));
}
}
None
}
const fn lod_key(lod: DetailLevel) -> &'static str {
match lod {
DetailLevel::Highest => "high_lod",
DetailLevel::High => "medium_lod",
DetailLevel::Medium => "low_lod",
DetailLevel::Low => "lowest_lod",
}
}
fn decode_faceted(
prim: &Primitive,
submeshes: &[OSD],
skin_data: Option<MeshSkinData>,
_lod: DetailLevel,
source: libremetaverse_types::UUID,
) -> Result<FacetedMesh, Error> {
if submeshes.len() > MAX_SUBMESHES {
return Err(render_error(source, "mesh submesh count exceeds limit"));
}
let mut faces = Vec::with_capacity(submeshes.len());
let mut total_vertices = 0usize;
let mut total_indices = 0usize;
for (index, submesh) in submeshes.iter().enumerate() {
if let Some(face) = decode_submesh(prim, submesh, index, skin_data.as_ref(), source)? {
total_vertices = checked_total(
total_vertices,
face.vertices.len(),
MAX_TOTAL_VERTICES,
source,
"mesh vertex budget exceeded",
)?;
total_indices = checked_total(
total_indices,
face.indices.len(),
MAX_TOTAL_INDICES,
source,
"mesh index budget exceeded",
)?;
faces.push(face);
}
}
Ok(FacetedMesh { faces, skin_data })
}
#[allow(clippy::too_many_lines)] // One ordered pass validates parallel mesh domains.
fn decode_submesh(
prim: &Primitive,
value: &OSD,
face_index: usize,
skin: Option<&MeshSkinData>,
source: libremetaverse_types::UUID,
) -> Result<Option<Face>, Error> {
let map = as_map(value).ok_or_else(|| render_error(source, "mesh submesh is not a map"))?;
if map.get("NoGeometry").and_then(osd_boolean).unwrap_or(false) {
return Ok(None);
}
let positions = map
.get("Position")
.and_then(osd_binary)
.ok_or_else(|| render_error(source, "mesh positions are missing"))?;
if positions.is_empty() || !positions.len().is_multiple_of(6) {
return Err(render_error(source, "mesh position buffer is malformed"));
}
let vertex_count = positions.len() / 6;
if vertex_count > MAX_FACE_VERTICES {
return Err(render_error(
source,
"mesh face exceeds 16-bit vertex range",
));
}
let (position_min, position_max) = domain3(
map.get("PositionDomain"),
Vector3 {
x: -0.5,
y: -0.5,
z: -0.5,
},
Vector3 {
x: 0.5,
y: 0.5,
z: 0.5,
},
source,
"invalid mesh position domain",
)?;
let normals = map.get("Normal").and_then(osd_binary);
if normals.is_some_and(|bytes| bytes.len() != vertex_count * 6) {
return Err(render_error(source, "mesh normal buffer is misaligned"));
}
let tex_coords = map.get("TexCoord0").and_then(osd_binary);
if tex_coords.is_some_and(|bytes| bytes.len() != vertex_count * 4) {
return Err(render_error(source, "mesh primary UV buffer is misaligned"));
}
let (primary_min, primary_max) = if tex_coords.is_some() {
domain2(
Some(
map.get("TexCoord0Domain")
.ok_or_else(|| render_error(source, "primary UV domain is missing"))?,
),
Vector2::zero(),
Vector2::zero(),
source,
"invalid primary UV domain",
)?
} else {
(Vector2::zero(), Vector2::zero())
};
let tex_coords1 = map.get("TexCoord1").and_then(osd_binary);
if tex_coords1.is_some_and(|bytes| bytes.len() != vertex_count * 4) {
return Err(render_error(
source,
"mesh secondary UV buffer is misaligned",
));
}
let (secondary_min, secondary_max) = if tex_coords1.is_some() {
domain2(
map.get("TexCoord1Domain"),
Vector2::zero(),
Vector2::zero(),
source,
"invalid secondary UV domain",
)?
} else {
(Vector2::zero(), Vector2::zero())
};
let tangent_bytes = map.get("Tangent").and_then(osd_binary);
if tangent_bytes.is_some_and(|bytes| bytes.len() != vertex_count * 8) {
return Err(render_error(source, "mesh tangent buffer is misaligned"));
}
let mut vertices = Vec::with_capacity(vertex_count);
let mut secondary = tex_coords1.map(|_| Vec::with_capacity(vertex_count));
for index in 0..vertex_count {
let p = index * 6;
let t = index * 4;
let position = Vector3 {
x: dequantize(
read_u16(positions, p, source)?,
position_min.x,
position_max.x,
),
y: dequantize(
read_u16(positions, p + 2, source)?,
position_min.y,
position_max.y,
),
z: dequantize(
read_u16(positions, p + 4, source)?,
position_min.z,
position_max.z,
),
};
let normal = if let Some(bytes) = normals {
normalize3(
Vector3 {
x: dequantize(read_u16(bytes, p, source)?, -1.0, 1.0),
y: dequantize(read_u16(bytes, p + 2, source)?, -1.0, 1.0),
z: dequantize(read_u16(bytes, p + 4, source)?, -1.0, 1.0),
},
Vector3::unit_z(),
)
} else {
Vector3::zero()
};
let tex_coord = if let Some(bytes) = tex_coords {
Vector2 {
x: dequantize(read_u16(bytes, t, source)?, primary_min.x, primary_max.x),
y: dequantize(
read_u16(bytes, t + 2, source)?,
primary_min.y,
primary_max.y,
),
}
} else {
Vector2::zero()
};
vertices.push(checked_vertex(position, normal, tex_coord, source)?);
if let (Some(bytes), Some(output)) = (tex_coords1, secondary.as_mut()) {
output.push(Vector2 {
x: dequantize(
read_u16(bytes, t, source)?,
secondary_min.x,
secondary_max.x,
),
y: dequantize(
read_u16(bytes, t + 2, source)?,
secondary_min.y,
secondary_max.y,
),
});
}
}
let triangles = map
.get("TriangleList")
.and_then(osd_binary)
.ok_or_else(|| render_error(source, "mesh triangle list is missing"))?;
if !triangles.len().is_multiple_of(6) || triangles.len() / 2 > MAX_TOTAL_INDICES {
return Err(render_error(source, "mesh triangle buffer is malformed"));
}
let mut indices = Vec::with_capacity(triangles.len() / 2);
for offset in (0..triangles.len()).step_by(6) {
let a = read_u16(triangles, offset, source)?;
let b = read_u16(triangles, offset + 2, source)?;
let c = read_u16(triangles, offset + 4, source)?;
for index in [a, b, c] {
if usize::from(index) >= vertex_count {
return Err(render_error(source, "mesh triangle index is out of range"));
}
indices.push(index);
}
}
if normals.is_none() {
generate_normals(&mut vertices, &indices);
}
let tangents = if let Some(bytes) = tangent_bytes {
decode_tangents(bytes, vertex_count, source)?
} else {
generate_tangents(&vertices, &indices)
};
let weights = match (skin, map.get("Weights").and_then(osd_binary)) {
(Some(skin), Some(bytes)) => Some(decode_weights(
bytes,
skin.joint_names.len(),
vertex_count,
source,
)?),
(None, Some(_)) => {
return Err(render_error(source, "mesh weights have no skin section"));
}
_ => None,
};
let normalized_scale = map
.get("NormalizedScale")
.map(|value| osd_vector3(value, source, "invalid normalized face scale"))
.transpose()?
.unwrap_or(Vector3::one());
let texture = texture_for_face(prim, face_index, source)?;
let mut face = make_face(
i32::try_from(face_index).map_err(|_| render_error(source, "mesh face id overflow"))?,
vertices,
indices,
texture,
Object::opaque(MeshFaceAux { tangents }),
normalized_scale,
source,
)?;
face.tex_coords1 = secondary;
face.weights = weights;
Ok(Some(face))
}
fn decode_skin(value: &OSD, source: libremetaverse_types::UUID) -> Result<MeshSkinData, Error> {
let map =
as_map(value).ok_or_else(|| render_error(source, "mesh skin section is not a map"))?;
let names = match map.get("joint_names") {
Some(OSD::Array(values)) if values.len() <= MAX_JOINTS => values
.iter()
.map(|value| {
value
.as_string()
.map_err(|_| render_error(source, "invalid skin joint name"))
})
.collect::<Result<Vec<_>, _>>()?,
Some(OSD::Array(_)) => return Err(render_error(source, "skin joint count exceeds limit")),
Some(_) => return Err(render_error(source, "skin joint names are not an array")),
None => Vec::new(),
};
let mut data = MeshSkinData::new()?;
data.joint_names = names;
data.inverse_bind_matrices = decode_matrix_array(
map.get("inverse_bind_matrix"),
data.joint_names.len(),
false,
source,
"invalid inverse bind matrices",
)?;
data.alt_inverse_bind_matrices = decode_matrix_array(
map.get("alt_inverse_bind_matrix"),
data.joint_names.len(),
true,
source,
"invalid alternate inverse bind matrices",
)?;
data.bind_shape_matrix = match map.get("bind_shape_matrix") {
Some(value) => decode_matrix(value, source, "invalid bind shape matrix")?,
None => identity_matrix().to_vec(),
};
data.pelvis_offset = match map.get("pelvis_offset") {
Some(value) => {
let value = value
.as_real()
.map_err(|_| render_error(source, "invalid pelvis offset"))?
as f32;
if !value.is_finite() {
return Err(render_error(source, "non-finite pelvis offset"));
}
value
}
None => 0.0,
};
data.lock_scale_if_joint_position = map
.get("lock_scale_if_joint_position")
.map(OSD::as_boolean)
.transpose()
.map_err(|_| render_error(source, "invalid skin scale lock"))?
.unwrap_or(false);
Ok(data)
}
fn decode_matrix_array(
value: Option<&OSD>,
joint_count: usize,
optional: bool,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<Vec<f32>, Error> {
let Some(value) = value else {
if optional {
return Ok(Vec::new());
}
return Ok((0..joint_count).flat_map(|_| identity_matrix()).collect());
};
let OSD::Array(matrices) = value else {
return Err(render_error(source, context));
};
if matrices.len() != joint_count {
return Err(render_error(source, context));
}
let mut output = Vec::with_capacity(joint_count * 16);
for matrix in matrices {
output.extend(decode_matrix(matrix, source, context)?);
}
Ok(output)
}
fn decode_matrix(
value: &OSD,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<Vec<f32>, Error> {
let OSD::Array(values) = value else {
return Err(render_error(source, context));
};
if values.len() != 16 {
return Err(render_error(source, context));
}
values
.iter()
.map(|value| {
let value = value.as_real().map_err(|_| render_error(source, context))? as f32;
if value.is_finite() {
Ok(value)
} else {
Err(render_error(source, context))
}
})
.collect()
}
fn decode_weights(
bytes: &[u8],
joint_count: usize,
vertex_count: usize,
source: libremetaverse_types::UUID,
) -> Result<Vec<VertexWeight>, Error> {
if joint_count == 0 || bytes.len() > vertex_count.saturating_mul(13) {
return Err(render_error(source, "mesh skin weights are out of range"));
}
let mut cursor = 0usize;
let mut output = Vec::with_capacity(vertex_count);
for _ in 0..vertex_count {
if cursor >= bytes.len() {
output.push(default_weight());
continue;
}
let mut joints = [0_i32; 4];
let mut weights = [0.0_f32; 4];
let mut count = 0usize;
while cursor < bytes.len() && count < 4 {
let joint = bytes[cursor];
cursor += 1;
if joint == 0xff {
break;
}
if usize::from(joint) >= joint_count {
return Err(render_error(
source,
"mesh skin joint index is out of range",
));
}
let end = cursor
.checked_add(2)
.ok_or_else(|| render_error(source, "mesh weight offset overflow"))?;
let raw = bytes
.get(cursor..end)
.ok_or_else(|| render_error(source, "truncated mesh skin weight"))?;
cursor = end;
joints[count] = i32::from(joint);
weights[count] =
(f32::from(u16::from_le_bytes([raw[0], raw[1]])) / 65_535.0).clamp(0.001, 0.999);
count += 1;
}
let total: f32 = weights.iter().sum();
if total > 0.0 {
for weight in &mut weights {
*weight /= total;
}
} else {
weights[0] = 1.0;
}
output.push(VertexWeight {
joint0: joints[0],
joint1: joints[1],
joint2: joints[2],
joint3: joints[3],
weight0: weights[0],
weight1: weights[1],
weight2: weights[2],
weight3: weights[3],
});
}
if cursor != bytes.len() {
return Err(render_error(source, "trailing mesh skin weights"));
}
Ok(output)
}
const fn default_weight() -> VertexWeight {
VertexWeight {
joint0: 0,
joint1: 0,
joint2: 0,
joint3: 0,
weight0: 1.0,
weight1: 0.0,
weight2: 0.0,
weight3: 0.0,
}
}
fn decode_tangents(
bytes: &[u8],
vertex_count: usize,
source: libremetaverse_types::UUID,
) -> Result<Vec<Vector4>, Error> {
let mut tangents = Vec::with_capacity(vertex_count);
for index in 0..vertex_count {
let offset = index * 8;
let tangent = normalize3(
Vector3 {
x: dequantize(read_u16(bytes, offset, source)?, -1.0, 1.0),
y: dequantize(read_u16(bytes, offset + 2, source)?, -1.0, 1.0),
z: dequantize(read_u16(bytes, offset + 4, source)?, -1.0, 1.0),
},
Vector3::unit_x(),
);
let handedness = dequantize(read_u16(bytes, offset + 6, source)?, -1.0, 1.0);
tangents.push(Vector4 {
x: tangent.x,
y: tangent.y,
z: tangent.z,
w: if handedness < 0.0 { -1.0 } else { 1.0 },
});
}
Ok(tangents)
}
fn generate_normals(vertices: &mut [Vertex], indices: &[u16]) {
let mut normals = vec![Vector3::zero(); vertices.len()];
for triangle in indices.chunks_exact(3) {
let a = vertices[usize::from(triangle[0])].position;
let b = vertices[usize::from(triangle[1])].position;
let c = vertices[usize::from(triangle[2])].position;
let normal = cross(sub3(b, a), sub3(c, a));
for index in triangle {
normals[usize::from(*index)] = add3(normals[usize::from(*index)], normal);
}
}
for (vertex, normal) in vertices.iter_mut().zip(normals) {
vertex.normal = normalize3(normal, Vector3::unit_z());
}
}
fn generate_tangents(vertices: &[Vertex], indices: &[u16]) -> Vec<Vector4> {
let mut tangent_sum = vec![Vector3::zero(); vertices.len()];
let mut bitangent_sum = vec![Vector3::zero(); vertices.len()];
for triangle in indices.chunks_exact(3) {
let ia = usize::from(triangle[0]);
let ib = usize::from(triangle[1]);
let ic = usize::from(triangle[2]);
let edge1 = sub3(vertices[ib].position, vertices[ia].position);
let edge2 = sub3(vertices[ic].position, vertices[ia].position);
let duv1 = sub2(vertices[ib].tex_coord, vertices[ia].tex_coord);
let duv2 = sub2(vertices[ic].tex_coord, vertices[ia].tex_coord);
let determinant = duv1.x * duv2.y - duv1.y * duv2.x;
if determinant.abs() <= f32::EPSILON || !determinant.is_finite() {
continue;
}
let inverse = 1.0 / determinant;
let tangent = scale3(sub3(scale3(edge1, duv2.y), scale3(edge2, duv1.y)), inverse);
let bitangent = scale3(sub3(scale3(edge2, duv1.x), scale3(edge1, duv2.x)), inverse);
for index in [ia, ib, ic] {
tangent_sum[index] = add3(tangent_sum[index], tangent);
bitangent_sum[index] = add3(bitangent_sum[index], bitangent);
}
}
vertices
.iter()
.enumerate()
.map(|(index, vertex)| {
let projected = sub3(
tangent_sum[index],
scale3(vertex.normal, dot(vertex.normal, tangent_sum[index])),
);
let tangent = normalize3(projected, orthogonal_tangent(vertex.normal));
let handedness = if dot(cross(vertex.normal, tangent), bitangent_sum[index]) < 0.0 {
-1.0
} else {
1.0
};
Vector4 {
x: tangent.x,
y: tangent.y,
z: tangent.z,
w: handedness,
}
})
.collect()
}
fn apply_sculpt_modifiers(prim: &Primitive, mesh: &mut FacetedMesh) -> Result<(), Error> {
let Some(sculpt) = prim.sculpt.as_ref() else {
return Ok(());
};
let reflect = sculpt.mirror();
let invert = sculpt.invert();
let reverse = reflect ^ invert;
if !reflect && !invert {
return Ok(());
}
for face in &mut mesh.faces {
for vertex in &mut face.vertices {
if reflect {
vertex.position.x = -vertex.position.x;
vertex.normal.x = -vertex.normal.x;
}
if invert {
vertex.normal = scale3(vertex.normal, -1.0);
}
}
if let Some(aux) = face.user_data.downcast_ref::<MeshFaceAux>() {
let tangents = aux
.tangents
.iter()
.map(|tangent| Vector4 {
x: if reflect { -tangent.x } else { tangent.x },
y: tangent.y,
z: tangent.z,
w: if reverse { -tangent.w } else { tangent.w },
})
.collect();
face.user_data = Object::opaque(MeshFaceAux { tangents });
}
if reverse {
for triangle in face.indices.chunks_exact_mut(3) {
triangle.swap(1, 2);
}
}
update_face_bounds(face)?;
}
Ok(())
}
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_FACE_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);
}
Ok(SimpleMesh { vertices, indices })
}
fn make_face(
id: i32,
vertices: Vec<Vertex>,
indices: Vec<u16>,
texture_face: PrimitiveTextureEntryFace,
user_data: Object,
normalized_scale: Vector3,
source: libremetaverse_types::UUID,
) -> Result<Face, Error> {
if vertices.is_empty() {
return Err(render_error(source, "render face has no vertices"));
}
let mut face = Face {
begin_s: 0,
begin_t: 0,
center: Vector3::zero(),
edge: Vec::new(),
id,
indices,
mask: FaceMask::SINGLE,
max_extent: Vector3::zero(),
min_extent: Vector3::zero(),
normalized_scale,
num_s: 0,
num_t: 0,
tex_coords1: None,
texture_face,
user_data,
vertices,
weights: None,
};
update_face_bounds(&mut face)?;
Ok(face)
}
fn update_face_bounds(face: &mut Face) -> Result<(), Error> {
let Some(first) = face.vertices.first() else {
return Err(Error::Argument);
};
let mut min = first.position;
let mut max = first.position;
for vertex in &face.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);
}
face.min_extent = min;
face.max_extent = max;
face.center = Vector3 {
x: (min.x + max.x) * 0.5,
y: (min.y + max.y) * 0.5,
z: (min.z + max.z) * 0.5,
};
Ok(())
}
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 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>,
a: i32,
b: i32,
c: i32,
vertex_count: usize,
source: libremetaverse_types::UUID,
) -> Result<(), Error> {
for index in [a, b, c] {
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 domain3(
value: Option<&OSD>,
default_min: Vector3,
default_max: Vector3,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<(Vector3, Vector3), Error> {
let Some(value) = value else {
return Ok((default_min, default_max));
};
let map = as_map(value).ok_or_else(|| render_error(source, context))?;
let min = map
.get("Min")
.ok_or_else(|| render_error(source, context))
.and_then(|value| osd_vector3(value, source, context))?;
let max = map
.get("Max")
.ok_or_else(|| render_error(source, context))
.and_then(|value| osd_vector3(value, source, context))?;
validate_domain3(min, max, source, context)?;
Ok((min, max))
}
fn domain2(
value: Option<&OSD>,
default_min: Vector2,
default_max: Vector2,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<(Vector2, Vector2), Error> {
let Some(value) = value else {
return Ok((default_min, default_max));
};
let map = as_map(value).ok_or_else(|| render_error(source, context))?;
let min = map
.get("Min")
.ok_or_else(|| render_error(source, context))?
.as_vector2()
.map_err(|_| render_error(source, context))?;
let max = map
.get("Max")
.ok_or_else(|| render_error(source, context))?
.as_vector2()
.map_err(|_| render_error(source, context))?;
if !finite2(min) || !finite2(max) || min.x > max.x || min.y > max.y {
return Err(render_error(source, context));
}
Ok((min, max))
}
fn validate_domain3(
min: Vector3,
max: Vector3,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<(), Error> {
if !finite3(min) || !finite3(max) || min.x > max.x || min.y > max.y || min.z > max.z {
Err(render_error(source, context))
} else {
Ok(())
}
}
fn decode_quantized_positions(
bytes: &[u8],
min: Vector3,
max: Vector3,
source: libremetaverse_types::UUID,
) -> Result<Vec<Vector3>, Error> {
if !bytes.len().is_multiple_of(6) || bytes.len() / 6 > MAX_HULL_VERTICES {
return Err(render_error(source, "convex position buffer is malformed"));
}
(0..bytes.len())
.step_by(6)
.map(|offset| {
Ok(Vector3 {
x: dequantize(read_u16(bytes, offset, source)?, min.x, max.x),
y: dequantize(read_u16(bytes, offset + 2, source)?, min.y, max.y),
z: dequantize(read_u16(bytes, offset + 4, source)?, min.z, max.z),
})
})
.collect()
}
fn read_u16(bytes: &[u8], offset: usize, source: libremetaverse_types::UUID) -> Result<u16, Error> {
let bytes = bytes
.get(offset..offset + 2)
.ok_or_else(|| render_error(source, "truncated quantized mesh value"))?;
Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
}
fn checked_total(
current: usize,
additional: usize,
limit: usize,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<usize, Error> {
let total = current
.checked_add(additional)
.ok_or_else(|| render_error(source, context))?;
if total > limit {
Err(render_error(source, context))
} else {
Ok(total)
}
}
fn osd_vector3(
value: &OSD,
source: libremetaverse_types::UUID,
context: &'static str,
) -> Result<Vector3, Error> {
let value = value
.as_vector3()
.map_err(|_| render_error(source, context))?;
if finite3(value) {
Ok(value)
} else {
Err(render_error(source, context))
}
}
const fn as_map(value: &OSD) -> Option<&HashMap<String, OSD>> {
if let OSD::Map(map) = value {
Some(map)
} else {
None
}
}
const fn osd_binary(value: &OSD) -> Option<&Vec<u8>> {
if let OSD::Binary(bytes) = value {
Some(bytes)
} else {
None
}
}
const fn osd_integer(value: &OSD) -> Option<i32> {
if let OSD::Integer(value) = value {
Some(*value)
} else {
None
}
}
const fn osd_boolean(value: &OSD) -> Option<bool> {
if let OSD::Boolean(value) = value {
Some(*value)
} else {
None
}
}
const fn dequantize(value: u16, min: f32, max: f32) -> f32 {
min + (value as f32 / 65_535.0) * (max - min)
}
fn normalize3(value: Vector3, fallback: Vector3) -> Vector3 {
let length_squared = dot(value, value);
if length_squared > f32::EPSILON && length_squared.is_finite() {
scale3(value, length_squared.sqrt().recip())
} else {
fallback
}
}
fn orthogonal_tangent(normal: Vector3) -> Vector3 {
let axis = if normal.z.abs() < 0.9 {
Vector3::unit_z()
} else {
Vector3::unit_y()
};
normalize3(cross(axis, normal), Vector3::unit_x())
}
const fn identity_matrix() -> [f32; 16] {
[
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
}
const fn add3(left: Vector3, right: Vector3) -> Vector3 {
Vector3 {
x: left.x + right.x,
y: left.y + right.y,
z: left.z + right.z,
}
}
const fn sub3(left: Vector3, right: Vector3) -> Vector3 {
Vector3 {
x: left.x - right.x,
y: left.y - right.y,
z: left.z - right.z,
}
}
const fn sub2(left: Vector2, right: Vector2) -> Vector2 {
Vector2 {
x: left.x - right.x,
y: left.y - right.y,
}
}
const fn scale3(value: Vector3, scalar: f32) -> Vector3 {
Vector3 {
x: value.x * scalar,
y: value.y * scalar,
z: value.z * scalar,
}
}
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,
}
}
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 render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
Error::Rendering { source, context }
}