use std::io;
use draco_core::draco_types::DataType;
use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
use draco_core::mesh::Mesh;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GltfError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid GLB: {0}")]
InvalidGlb(String),
#[error("Invalid glTF: {0}")]
InvalidGltf(String),
#[error("Draco decode error: {0}")]
DracoDecode(#[source] draco_core::DracoError),
#[error("Draco encode error: {0}")]
DracoEncode(#[source] draco_core::DracoError),
#[error("Unsupported feature: {0}")]
Unsupported(String),
#[error("External resource denied: {0}")]
ExternalResourceDenied(String),
#[error("Resource limit exceeded: {0}")]
ResourceLimitExceeded(String),
#[error("Opaque binary reference: {0}")]
OpaqueBinaryReference(String),
#[error("Invalid compression options: {0}")]
InvalidOptions(String),
}
pub type Result<T> = std::result::Result<T, GltfError>;
pub(crate) const GLTF_MODE_POINTS: u32 = 0;
pub(crate) const GLTF_MODE_TRIANGLES: u32 = 4;
pub(crate) const GLTF_COMPONENT_BYTE: u32 = 5120;
pub(crate) const GLTF_COMPONENT_UNSIGNED_BYTE: u32 = 5121;
pub(crate) const GLTF_COMPONENT_SHORT: u32 = 5122;
pub(crate) const GLTF_COMPONENT_UNSIGNED_SHORT: u32 = 5123;
#[cfg(feature = "gltf-reader")]
pub(crate) const GLTF_COMPONENT_UNSIGNED_INT: u32 = 5125;
pub(crate) const GLTF_COMPONENT_FLOAT: u32 = 5126;
pub struct DecodedAccessor {
count: usize,
num_components: u8,
data_type: DataType,
normalized: bool,
bytes: Vec<u8>,
}
impl DecodedAccessor {
pub fn new(
count: usize,
num_components: u8,
data_type: DataType,
normalized: bool,
bytes: Vec<u8>,
) -> Result<Self> {
let expected = count
.checked_mul(num_components as usize)
.and_then(|count| count.checked_mul(data_type.byte_length()))
.ok_or_else(|| GltfError::InvalidGltf("accessor byte size overflow".into()))?;
if bytes.len() != expected {
return Err(GltfError::InvalidGltf(format!(
"accessor has {} bytes, expected {expected}",
bytes.len()
)));
}
Ok(Self {
count,
num_components,
data_type,
normalized,
bytes,
})
}
fn gather(&self, indices: &[u32]) -> Result<Self> {
let stride = (self.num_components as usize)
.checked_mul(self.data_type.byte_length())
.ok_or_else(|| GltfError::InvalidGltf("accessor stride overflow".into()))?;
let byte_len = indices
.len()
.checked_mul(stride)
.ok_or_else(|| GltfError::InvalidGltf("gathered accessor size overflow".into()))?;
let mut bytes = Vec::new();
bytes.try_reserve_exact(byte_len).map_err(|_| {
GltfError::ResourceLimitExceeded("gathered accessor allocation failed".into())
})?;
for &index in indices {
let index = index as usize;
if index >= self.count {
return Err(GltfError::InvalidGltf(format!(
"Accessor index {} out of bounds for {} values",
index, self.count
)));
}
let offset = index
.checked_mul(stride)
.ok_or_else(|| GltfError::InvalidGltf("accessor offset overflow".into()))?;
let end = offset
.checked_add(stride)
.filter(|end| *end <= self.bytes.len())
.ok_or_else(|| GltfError::InvalidGltf("accessor bytes are truncated".into()))?;
bytes.extend_from_slice(&self.bytes[offset..end]);
}
Ok(Self {
count: indices.len(),
num_components: self.num_components,
data_type: self.data_type,
normalized: self.normalized,
bytes,
})
}
}
pub trait AccessorSource {
fn read_attribute(
&self,
accessor: usize,
expected_types: &[&str],
allowed_component_types: &[u32],
) -> Result<DecodedAccessor>;
fn read_indices(&self, accessor: usize) -> Result<Vec<u32>>;
}
#[derive(Clone, Copy)]
pub(crate) struct SemanticSpec {
pub(crate) attribute_type: GeometryAttributeType,
pub(crate) expected_accessor_types: &'static [&'static str],
pub(crate) allowed_component_types: &'static [u32],
normalization: NormalizationPolicy,
}
#[derive(Clone, Copy)]
enum NormalizationPolicy {
Forbidden,
RequiredForInteger,
Generic,
}
const FLOAT_ONLY: &[u32] = &[GLTF_COMPONENT_FLOAT];
const TEXCOORD_COMPONENT_TYPES: &[u32] = &[
GLTF_COMPONENT_FLOAT,
GLTF_COMPONENT_UNSIGNED_BYTE,
GLTF_COMPONENT_UNSIGNED_SHORT,
];
const COLOR_COMPONENT_TYPES: &[u32] = &[
GLTF_COMPONENT_FLOAT,
GLTF_COMPONENT_UNSIGNED_BYTE,
GLTF_COMPONENT_UNSIGNED_SHORT,
];
const JOINT_COMPONENT_TYPES: &[u32] =
&[GLTF_COMPONENT_UNSIGNED_BYTE, GLTF_COMPONENT_UNSIGNED_SHORT];
const WEIGHT_COMPONENT_TYPES: &[u32] = &[
GLTF_COMPONENT_FLOAT,
GLTF_COMPONENT_UNSIGNED_BYTE,
GLTF_COMPONENT_UNSIGNED_SHORT,
];
const GENERIC_COMPONENT_TYPES: &[u32] = &[
GLTF_COMPONENT_BYTE,
GLTF_COMPONENT_UNSIGNED_BYTE,
GLTF_COMPONENT_SHORT,
GLTF_COMPONENT_UNSIGNED_SHORT,
GLTF_COMPONENT_FLOAT,
];
pub fn decode_geometry<S: AccessorSource>(
src: &S,
mode: u32,
attributes: &[(String, usize)],
indices: Option<usize>,
) -> Result<(Mesh, Vec<(String, u32)>)> {
if mode != GLTF_MODE_TRIANGLES && mode != GLTF_MODE_POINTS {
return Err(GltfError::Unsupported(format!(
"Primitive mode {} not supported (only POINTS=0 and TRIANGLES=4)",
mode
)));
}
let pos_accessor_idx = attributes
.iter()
.find(|(semantic, _)| semantic == "POSITION")
.map(|(_, accessor)| *accessor)
.ok_or_else(|| GltfError::InvalidGltf("primitive has no POSITION attribute".into()))?;
let positions = src.read_attribute(pos_accessor_idx, &["VEC3"], &[GLTF_COMPONENT_FLOAT])?;
validate_decoded_semantic("POSITION", &positions)?;
let mut mesh = Mesh::new();
let point_indices = if mode == GLTF_MODE_POINTS {
indices.map(|idx| src.read_indices(idx)).transpose()?
} else {
None
};
let positions = if let Some(idx) = &point_indices {
positions.gather(idx)?
} else {
positions
};
mesh.set_num_points(positions.count);
let mut semantics: Vec<(String, u32)> = Vec::new();
semantics.try_reserve_exact(attributes.len()).map_err(|_| {
GltfError::ResourceLimitExceeded("attribute semantic table allocation failed".into())
})?;
let pos_id = add_decoded_attribute(&mut mesh, GeometryAttributeType::Position, positions)?;
semantics.push(("POSITION".to_string(), pos_id as u32));
if mode == GLTF_MODE_TRIANGLES {
if let Some(indices_accessor_idx) = indices {
let indices = src.read_indices(indices_accessor_idx)?;
if indices.len() % 3 != 0 {
return Err(GltfError::InvalidGltf(
"Index count not divisible by 3 for triangles".into(),
));
}
for &index in &indices {
if index as usize >= mesh.num_points() {
return Err(GltfError::InvalidGltf(format!(
"Triangle index {} out of bounds for {} points",
index,
mesh.num_points()
)));
}
}
let num_faces = indices.len() / 3;
mesh.try_set_num_faces(num_faces)
.map_err(GltfError::DracoEncode)?;
for (face_id, face) in indices.chunks_exact(3).enumerate() {
mesh.set_face_from_indices(face_id, [face[0], face[1], face[2]]);
}
} else {
if !mesh.num_points().is_multiple_of(3) {
return Err(GltfError::InvalidGltf(
"Non-indexed primitive point count not divisible by 3".into(),
));
}
let num_faces = mesh.num_points() / 3;
mesh.try_set_num_faces(num_faces)
.map_err(GltfError::DracoEncode)?;
for face_id in 0..num_faces {
let base = face_id
.checked_mul(3)
.and_then(|base| u32::try_from(base).ok())
.ok_or_else(|| {
GltfError::InvalidGltf(
"Non-indexed primitive exceeds Draco's u32 point-id limit".into(),
)
})?;
let second = base
.checked_add(1)
.ok_or_else(|| GltfError::InvalidGltf("Triangle point-id overflow".into()))?;
let third = base
.checked_add(2)
.ok_or_else(|| GltfError::InvalidGltf("Triangle point-id overflow".into()))?;
mesh.set_face_from_indices(face_id, [base, second, third]);
}
}
}
if let Some(normal_idx) = attributes
.iter()
.find(|(semantic, _)| semantic == "NORMAL")
.map(|(_, accessor)| *accessor)
{
let spec = supported_semantic_spec("NORMAL")?;
let normal_id = read_and_add_standard_attribute(
&mut mesh,
src,
normal_idx,
"NORMAL",
spec,
point_indices.as_deref(),
)?;
semantics.push(("NORMAL".to_string(), normal_id as u32));
}
let mut sorted: Vec<&(String, usize)> = Vec::new();
sorted.try_reserve_exact(attributes.len()).map_err(|_| {
GltfError::ResourceLimitExceeded("attribute sort table allocation failed".into())
})?;
sorted.extend(attributes.iter());
sorted.sort_by(|(left, _), (right, _)| left.cmp(right));
for (semantic, accessor_idx) in sorted {
if semantic == "POSITION" || semantic == "NORMAL" {
continue;
}
let attribute_spec = supported_semantic_spec(semantic)?;
let att_id = read_and_add_standard_attribute(
&mut mesh,
src,
*accessor_idx,
semantic,
attribute_spec,
point_indices.as_deref(),
)?;
semantics.push((semantic.clone(), att_id as u32));
}
mesh.deduplicate_point_ids();
Ok((mesh, semantics))
}
#[cfg(feature = "gltf-reader")]
pub(crate) fn add_named_attribute<S: AccessorSource>(
mesh: &mut Mesh,
src: &S,
semantic: &str,
accessor_idx: usize,
point_indices: Option<&[u32]>,
) -> Result<i32> {
let spec = supported_semantic_spec(semantic)?;
read_and_add_standard_attribute(mesh, src, accessor_idx, semantic, spec, point_indices)
}
fn read_and_add_standard_attribute<S: AccessorSource>(
mesh: &mut Mesh,
src: &S,
accessor_idx: usize,
semantic: &str,
spec: SemanticSpec,
point_indices: Option<&[u32]>,
) -> Result<i32> {
let decoded = src.read_attribute(
accessor_idx,
spec.expected_accessor_types,
spec.allowed_component_types,
)?;
validate_decoded_semantic(semantic, &decoded)?;
let decoded = if let Some(indices) = point_indices {
decoded.gather(indices)?
} else {
decoded
};
add_decoded_attribute(mesh, spec.attribute_type, decoded)
}
fn add_decoded_attribute(
mesh: &mut Mesh,
attribute_type: GeometryAttributeType,
decoded: DecodedAccessor,
) -> Result<i32> {
if decoded.count != mesh.num_points() {
return Err(GltfError::InvalidGltf(format!(
"Attribute {:?} has {} values but primitive has {} points",
attribute_type,
decoded.count,
mesh.num_points()
)));
}
let mut attribute = PointAttribute::new();
attribute
.try_init(
attribute_type,
decoded.num_components,
decoded.data_type,
decoded.normalized,
decoded.count,
)
.map_err(GltfError::DracoEncode)?;
if !attribute.buffer_mut().try_write(0, &decoded.bytes) {
return Err(GltfError::DracoEncode(draco_core::DracoError::BufferError(
"Decoded glTF attribute does not fit its Draco buffer".into(),
)));
}
Ok(mesh.add_attribute(attribute))
}
pub(crate) fn supported_semantic_spec(semantic: &str) -> Result<SemanticSpec> {
let spec = if semantic == "POSITION" {
SemanticSpec {
attribute_type: GeometryAttributeType::Position,
expected_accessor_types: &["VEC3"],
allowed_component_types: FLOAT_ONLY,
normalization: NormalizationPolicy::Forbidden,
}
} else if semantic == "NORMAL" {
SemanticSpec {
attribute_type: GeometryAttributeType::Normal,
expected_accessor_types: &["VEC3"],
allowed_component_types: FLOAT_ONLY,
normalization: NormalizationPolicy::Forbidden,
}
} else if semantic == "TANGENT" {
SemanticSpec {
attribute_type: GeometryAttributeType::Generic,
expected_accessor_types: &["VEC4"],
allowed_component_types: FLOAT_ONLY,
normalization: NormalizationPolicy::Forbidden,
}
} else if indexed_semantic(semantic, "TEXCOORD_") {
SemanticSpec {
attribute_type: GeometryAttributeType::TexCoord,
expected_accessor_types: &["VEC2"],
allowed_component_types: TEXCOORD_COMPONENT_TYPES,
normalization: NormalizationPolicy::RequiredForInteger,
}
} else if indexed_semantic(semantic, "COLOR_") {
SemanticSpec {
attribute_type: GeometryAttributeType::Color,
expected_accessor_types: &["VEC3", "VEC4"],
allowed_component_types: COLOR_COMPONENT_TYPES,
normalization: NormalizationPolicy::RequiredForInteger,
}
} else if indexed_semantic(semantic, "JOINTS_") {
SemanticSpec {
attribute_type: GeometryAttributeType::Generic,
expected_accessor_types: &["VEC4"],
allowed_component_types: JOINT_COMPONENT_TYPES,
normalization: NormalizationPolicy::Forbidden,
}
} else if indexed_semantic(semantic, "WEIGHTS_") {
SemanticSpec {
attribute_type: GeometryAttributeType::Generic,
expected_accessor_types: &["VEC4"],
allowed_component_types: WEIGHT_COMPONENT_TYPES,
normalization: NormalizationPolicy::RequiredForInteger,
}
} else if semantic.starts_with('_') && semantic.len() > 1 {
SemanticSpec {
attribute_type: GeometryAttributeType::Generic,
expected_accessor_types: &["SCALAR", "VEC2", "VEC3", "VEC4"],
allowed_component_types: GENERIC_COMPONENT_TYPES,
normalization: NormalizationPolicy::Generic,
}
} else {
return Err(GltfError::InvalidGltf(format!(
"invalid glTF attribute semantic {semantic}"
)));
};
Ok(spec)
}
fn indexed_semantic(semantic: &str, prefix: &str) -> bool {
semantic
.strip_prefix(prefix)
.is_some_and(|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
}
pub(crate) fn validate_semantic_accessor(
semantic: &str,
accessor_type: &str,
component_type: u32,
normalized: bool,
) -> Result<SemanticSpec> {
let spec = supported_semantic_spec(semantic)?;
if !spec.expected_accessor_types.contains(&accessor_type) {
return Err(GltfError::InvalidGltf(format!(
"{semantic} accessor type {accessor_type} is invalid"
)));
}
if !spec.allowed_component_types.contains(&component_type) {
return Err(GltfError::InvalidGltf(format!(
"{semantic} accessor componentType {component_type} is invalid"
)));
}
let integer = component_type != GLTF_COMPONENT_FLOAT;
let valid_normalized = match spec.normalization {
NormalizationPolicy::Forbidden => !normalized,
NormalizationPolicy::RequiredForInteger => normalized == integer,
NormalizationPolicy::Generic => !normalized || integer,
};
if !valid_normalized {
return Err(GltfError::InvalidGltf(format!(
"{semantic} accessor normalized={normalized} is invalid for componentType {component_type}"
)));
}
Ok(spec)
}
fn validate_decoded_semantic(semantic: &str, accessor: &DecodedAccessor) -> Result<()> {
validate_semantic_accessor(
semantic,
gltf_type_for_num_components(accessor.num_components)?,
component_type_for_data_type(accessor.data_type)?,
accessor.normalized,
)?;
Ok(())
}
pub(crate) fn gltf_type_for_num_components(num_components: u8) -> Result<&'static str> {
match num_components {
1 => Ok("SCALAR"),
2 => Ok("VEC2"),
3 => Ok("VEC3"),
4 => Ok("VEC4"),
_ => Err(GltfError::InvalidGltf(format!(
"Invalid accessor component count: {num_components}"
))),
}
}
pub(crate) fn component_type_for_data_type(data_type: DataType) -> Result<u32> {
match data_type {
DataType::Int8 => Ok(GLTF_COMPONENT_BYTE),
DataType::Uint8 => Ok(GLTF_COMPONENT_UNSIGNED_BYTE),
DataType::Int16 => Ok(GLTF_COMPONENT_SHORT),
DataType::Uint16 => Ok(GLTF_COMPONENT_UNSIGNED_SHORT),
#[cfg(feature = "gltf-reader")]
DataType::Uint32 => Ok(GLTF_COMPONENT_UNSIGNED_INT),
DataType::Float32 => Ok(GLTF_COMPONENT_FLOAT),
_ => Err(GltfError::Unsupported(format!(
"Unsupported Draco attribute data type for glTF: {data_type:?}"
))),
}
}