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
1437 lines
46 KiB
Rust
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 }
|
|
}
|