use std::path::Path;
use draco_core::{DecoderBuffer, FaceIndex, Mesh, MeshDecoder, PointIndex};
use draco_io::{decode_geometry, AccessorSource, DecodedAccessor, GltfError};
#[cfg(not(feature = "image"))]
use gltf::buffer;
use serde_json::Value;
pub use gltf;
const KHR_DRACO: &str = "KHR_draco_mesh_compression";
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("glTF error: {0}")]
Gltf(#[from] gltf::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Draco decode error: {0}")]
Decode(String),
#[error("compression error: {0}")]
Compress(String),
#[error("Draco extension error: {0}")]
Extension(String),
#[error("glTF validation failed: {0:?}")]
Validation(Vec<String>),
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Import {
pub document: gltf::Document,
pub buffers: Vec<Vec<u8>>,
#[cfg(feature = "image")]
pub images: Vec<gltf::image::Data>,
}
impl Import {
pub fn draco_primitives(&self) -> impl Iterator<Item = (gltf::Mesh<'_>, gltf::Primitive<'_>)> {
self.document
.meshes()
.flat_map(|mesh| mesh.primitives().map(move |prim| (mesh.clone(), prim)))
.filter(|(_, prim)| is_draco(prim))
}
pub fn decode_primitive(&self, primitive: &gltf::Primitive<'_>) -> Result<Mesh> {
decode_primitive(&self.document, &self.buffers, primitive)
}
pub fn decompress_in_place(&mut self) -> Result<()> {
let mut doc_json = serde_json::to_value(self.document.clone().into_json())?;
let mut plans = Vec::new();
for (mesh_idx, mesh) in self.document.meshes().enumerate() {
for (prim_idx, primitive) in mesh.primitives().enumerate() {
if !is_draco(&primitive) {
continue;
}
let decoded = decode_primitive(&self.document, &self.buffers, &primitive)?;
let semantics = draco_attribute_map(&primitive)
.ok_or_else(|| Error::Extension("missing attribute map".into()))?;
let attrs = semantics
.into_iter()
.map(|(sem, draco_id)| {
let acc = doc_json["meshes"][mesh_idx]["primitives"][prim_idx]
["attributes"][&sem]
.as_u64()
.ok_or_else(|| {
Error::Extension(format!("attribute {sem} has no accessor"))
})?;
Ok((acc as usize, draco_id))
})
.collect::<Result<Vec<_>>>()?;
let indices_acc = doc_json["meshes"][mesh_idx]["primitives"][prim_idx]["indices"]
.as_u64()
.map(|i| i as usize);
plans.push((mesh_idx, prim_idx, decoded, attrs, indices_acc));
}
}
if plans.is_empty() {
return Ok(());
}
let new_buffer_index = doc_json["buffers"].as_array().map_or(0, |b| b.len());
let mut bin: Vec<u8> = Vec::new();
for (mesh_idx, prim_idx, mesh, attrs, indices_acc) in &plans {
for (acc_idx, draco_id) in attrs {
let bytes = attribute_bytes(mesh, *draco_id);
let view = push_view(&mut doc_json, new_buffer_index, &mut bin, &bytes);
set_accessor_view(&mut doc_json, *acc_idx, view, mesh.num_points());
}
if let Some(acc_idx) = indices_acc {
let bytes = index_bytes(mesh);
let view = push_view(&mut doc_json, new_buffer_index, &mut bin, &bytes);
set_accessor_view(&mut doc_json, *acc_idx, view, mesh.num_faces() * 3);
doc_json["accessors"][*acc_idx]["componentType"] = Value::from(5125u64);
}
if let Some(ext) = doc_json["meshes"][*mesh_idx]["primitives"][*prim_idx]
.get_mut("extensions")
.and_then(Value::as_object_mut)
{
ext.remove(KHR_DRACO);
}
}
if let Some(buffers) = doc_json["buffers"].as_array_mut() {
buffers.push(serde_json::json!({ "byteLength": bin.len() as u64 }));
}
for key in ["extensionsUsed", "extensionsRequired"] {
if let Some(arr) = doc_json.get_mut(key).and_then(Value::as_array_mut) {
arr.retain(|v| v.as_str() != Some(KHR_DRACO));
}
}
let root: gltf::json::Root = serde_json::from_value(doc_json)?;
self.document = gltf::Document::from_json_without_validation(root);
self.buffers.push(bin);
Ok(())
}
}
fn attribute_bytes(mesh: &Mesh, draco_id: u32) -> Vec<u8> {
let att = mesh.attribute(draco_id as i32);
let stride = att.byte_stride() as usize;
let num_points = mesh.num_points();
let mut out = Vec::with_capacity(num_points * stride);
let mut tmp = vec![0u8; stride];
for p in 0..num_points {
let value_index = att.mapped_index(PointIndex(p as u32));
att.buffer().read(value_index.0 as usize * stride, &mut tmp);
out.extend_from_slice(&tmp);
}
out
}
fn index_bytes(mesh: &Mesh) -> Vec<u8> {
let mut out = Vec::with_capacity(mesh.num_faces() * 3 * 4);
for f in 0..mesh.num_faces() {
for point in mesh.face(FaceIndex(f as u32)) {
out.extend_from_slice(&point.0.to_le_bytes());
}
}
out
}
fn push_view(doc: &mut Value, buffer_index: usize, bin: &mut Vec<u8>, bytes: &[u8]) -> usize {
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let offset = bin.len();
bin.extend_from_slice(bytes);
let views = doc["bufferViews"]
.as_array_mut()
.expect("bufferViews array");
let index = views.len();
views.push(serde_json::json!({
"buffer": buffer_index as u64,
"byteOffset": offset as u64,
"byteLength": bytes.len() as u64,
}));
index
}
fn set_accessor_view(doc: &mut Value, accessor: usize, view: usize, count: usize) {
let acc = &mut doc["accessors"][accessor];
acc["bufferView"] = Value::from(view as u64);
acc["byteOffset"] = Value::from(0u64);
acc["count"] = Value::from(count as u64);
}
#[cfg(not(target_arch = "wasm32"))]
pub fn import<P: AsRef<Path>>(path: P) -> Result<Import> {
let path = path.as_ref();
let bytes = std::fs::read(path)?;
import_slice(&bytes, path.parent())
}
pub fn import_slice(bytes: &[u8], base: Option<&Path>) -> Result<Import> {
let gltf::Gltf { document, blob } = gltf::Gltf::from_slice_without_validation(bytes)?;
validate(&document)?;
#[cfg(feature = "image")]
{
let resolved = gltf::import_buffers(&document, base, blob)?;
let images = gltf::import_images(&document, base, &resolved)?;
let buffers = resolved.into_iter().map(|d| d.0).collect();
Ok(Import {
document,
buffers,
images,
})
}
#[cfg(not(feature = "image"))]
{
let buffers = load_buffers(&document, blob, base)?;
Ok(Import { document, buffers })
}
}
#[cfg(not(feature = "image"))]
fn load_buffers(
document: &gltf::Document,
blob: Option<Vec<u8>>,
base: Option<&Path>,
) -> Result<Vec<Vec<u8>>> {
let mut blob = blob;
let mut out = Vec::new();
for buffer in document.buffers() {
let data = match buffer.source() {
buffer::Source::Bin => blob
.take()
.ok_or_else(|| Error::Extension("GLB buffer has no BIN chunk".into()))?,
buffer::Source::Uri(uri) => load_uri(uri, base)?,
};
out.push(data);
}
Ok(out)
}
#[cfg(not(feature = "image"))]
fn load_uri(uri: &str, base: Option<&Path>) -> Result<Vec<u8>> {
if let Some(rest) = uri.strip_prefix("data:") {
let comma = rest
.find(',')
.ok_or_else(|| Error::Extension("malformed data URI".into()))?;
if rest[..comma].contains(";base64") {
return base64_decode(&rest[comma + 1..])
.ok_or_else(|| Error::Extension("invalid base64 in data URI".into()));
}
return Err(Error::Extension(
"only base64 data URIs are supported without the `image` feature".into(),
));
}
match base {
#[cfg(not(target_arch = "wasm32"))]
Some(base) => Ok(std::fs::read(base.join(uri))?),
#[cfg(target_arch = "wasm32")]
Some(_) => Err(Error::Extension(
"external file URIs are not available on wasm".into(),
)),
None => Err(Error::Extension(
"external resource URI requires a base path".into(),
)),
}
}
#[cfg(not(feature = "image"))]
fn base64_decode(input: &str) -> Option<Vec<u8>> {
fn val(b: u8) -> Option<u8> {
match b {
b'A'..=b'Z' => Some(b - b'A'),
b'a'..=b'z' => Some(b - b'a' + 26),
b'0'..=b'9' => Some(b - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let mut out = Vec::with_capacity(input.len() / 4 * 3);
let mut acc = 0u32;
let mut bits = 0u32;
for &b in input.as_bytes() {
if b == b'=' || b.is_ascii_whitespace() {
continue;
}
let v = val(b)? as u32;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
Some(out)
}
pub fn validate(document: &gltf::Document) -> Result<()> {
use gltf::json::validation::Validate;
let root = document.clone().into_json();
let accessor_count = root.accessors.len();
let mut errors = Vec::new();
for (mi, mesh) in root.meshes.iter().enumerate() {
for (pi, prim) in mesh.primitives.iter().enumerate() {
for accessor in prim.attributes.values() {
if accessor.value() >= accessor_count {
errors.push(format!(
"IndexOutOfBounds at meshes[{mi}].primitives[{pi}].attributes"
));
}
}
if let Some(indices) = &prim.indices {
if indices.value() >= accessor_count {
errors.push(format!(
"IndexOutOfBounds at meshes[{mi}].primitives[{pi}].indices"
));
}
}
}
}
if !errors.is_empty() {
return Err(Error::Validation(errors));
}
let draco_accessors = draco_accessor_indices(document);
let mut errors = Vec::new();
root.validate(&root, gltf::json::Path::new, &mut |path, error| {
let location = path().to_string();
if location.contains(KHR_DRACO) {
return;
}
if format!("{error:?}") == "Missing" {
if let Some(idx) = accessor_buffer_view_index(&location) {
if draco_accessors.contains(&idx) {
return;
}
}
}
errors.push(format!("{error:?} at {location}"));
});
if errors.is_empty() {
Ok(())
} else {
Err(Error::Validation(errors))
}
}
fn draco_accessor_indices(document: &gltf::Document) -> std::collections::HashSet<usize> {
let mut set = std::collections::HashSet::new();
for mesh in document.meshes() {
for prim in mesh.primitives() {
if !is_draco(&prim) {
continue;
}
for (_, accessor) in prim.attributes() {
set.insert(accessor.index());
}
if let Some(indices) = prim.indices() {
set.insert(indices.index());
}
}
}
set
}
fn accessor_buffer_view_index(location: &str) -> Option<usize> {
let rest = location.strip_prefix("accessors[")?;
if !rest.ends_with("].bufferView") {
return None;
}
rest[..rest.find(']')?].parse().ok()
}
pub fn is_draco(primitive: &gltf::Primitive<'_>) -> bool {
primitive.extension_value(KHR_DRACO).is_some()
}
pub fn decode_primitive(
document: &gltf::Document,
buffers: &[Vec<u8>],
primitive: &gltf::Primitive<'_>,
) -> Result<Mesh> {
let ext = primitive
.extension_value(KHR_DRACO)
.ok_or_else(|| Error::Extension("primitive is not Draco-compressed".into()))?;
let view_index =
ext.get("bufferView")
.and_then(|v| v.as_u64())
.ok_or_else(|| Error::Extension("missing bufferView".into()))? as usize;
let view = document
.views()
.nth(view_index)
.ok_or_else(|| Error::Extension(format!("bufferView {view_index} out of range")))?;
let buffer = buffers
.get(view.buffer().index())
.ok_or_else(|| Error::Extension("buffer not resolved".into()))?;
let start = view.offset();
let end = start
.checked_add(view.length())
.filter(|&e| e <= buffer.len())
.ok_or_else(|| Error::Extension("Draco bufferView out of range".into()))?;
let mut mesh = Mesh::new();
MeshDecoder::new()
.decode(&mut DecoderBuffer::new(&buffer[start..end]), &mut mesh)
.map_err(|e| Error::Decode(format!("{e:?}")))?;
Ok(mesh)
}
pub fn draco_attribute_map(primitive: &gltf::Primitive<'_>) -> Option<Vec<(String, u32)>> {
let ext = primitive.extension_value(KHR_DRACO)?;
let map = ext.get("attributes")?.as_object()?;
Some(
map.iter()
.filter_map(|(k, v)| Some((k.clone(), v.as_u64()? as u32)))
.collect(),
)
}
pub fn compress(document: &gltf::Document, buffers: &[Vec<u8>]) -> Result<Vec<u8>> {
let doc_value = serde_json::to_value(document.clone().into_json())?;
let descriptors = primitive_descriptors(&doc_value);
let source = GltfRsSource { document, buffers };
let (mut out_doc, bin) =
draco_io::compress_gltf_value(doc_value, buffers, None, |mesh_idx, prim_idx| {
let (mode, attributes, indices) = descriptors
.get(&(mesh_idx, prim_idx))
.ok_or_else(|| GltfError::InvalidGltf("primitive descriptor missing".into()))?;
decode_geometry(&source, *mode, attributes, *indices)
})
.map_err(|e| Error::Compress(e.to_string()))?;
embed_single_buffer(&mut out_doc, &bin);
Ok(serde_json::to_vec(&out_doc)?)
}
type PrimitiveDescriptor = (u32, Vec<(String, usize)>, Option<usize>);
fn primitive_descriptors(
doc: &Value,
) -> std::collections::HashMap<(usize, usize), PrimitiveDescriptor> {
let mut out = std::collections::HashMap::new();
let Some(meshes) = doc.get("meshes").and_then(Value::as_array) else {
return out;
};
for (mesh_idx, mesh) in meshes.iter().enumerate() {
let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
continue;
};
for (prim_idx, prim) in primitives.iter().enumerate() {
let mode = prim.get("mode").and_then(Value::as_u64).unwrap_or(4) as u32;
let attributes = prim
.get("attributes")
.and_then(Value::as_object)
.map(|map| {
map.iter()
.filter_map(|(k, v)| Some((k.clone(), v.as_u64()? as usize)))
.collect()
})
.unwrap_or_default();
let indices = prim
.get("indices")
.and_then(Value::as_u64)
.map(|i| i as usize);
out.insert((mesh_idx, prim_idx), (mode, attributes, indices));
}
}
out
}
struct GltfRsSource<'a> {
document: &'a gltf::Document,
buffers: &'a [Vec<u8>],
}
impl GltfRsSource<'_> {
fn accessor(&self, index: usize) -> std::result::Result<gltf::Accessor<'_>, GltfError> {
self.document
.accessors()
.nth(index)
.ok_or_else(|| GltfError::InvalidGltf(format!("accessor {index} out of range")))
}
fn extract(
&self,
accessor: &gltf::Accessor<'_>,
row: usize,
) -> std::result::Result<Vec<u8>, GltfError> {
let overflow = || GltfError::InvalidGltf("accessor range overflow".into());
let view = accessor
.view()
.ok_or_else(|| GltfError::Unsupported("sparse accessors are not supported".into()))?;
let buffer = self
.buffers
.get(view.buffer().index())
.ok_or_else(|| GltfError::InvalidGltf("buffer not resolved".into()))?;
let view_end = view
.offset()
.checked_add(view.length())
.ok_or_else(overflow)?;
let stride = view.stride().unwrap_or(row);
let base = view
.offset()
.checked_add(accessor.offset())
.ok_or_else(overflow)?;
let mut out = Vec::with_capacity(accessor.count().saturating_mul(row));
for i in 0..accessor.count() {
let start = base
.checked_add(i.checked_mul(stride).ok_or_else(overflow)?)
.ok_or_else(overflow)?;
let end = start.checked_add(row).ok_or_else(overflow)?;
if end > view_end || end > buffer.len() {
return Err(GltfError::InvalidGltf("accessor out of bounds".into()));
}
out.extend_from_slice(&buffer[start..end]);
}
Ok(out)
}
}
impl AccessorSource for GltfRsSource<'_> {
fn read_attribute(
&self,
accessor_idx: usize,
expected_types: &[&str],
allowed_component_types: &[u32],
) -> std::result::Result<DecodedAccessor, GltfError> {
let accessor = self.accessor(accessor_idx)?;
let type_str = dimensions_str(accessor.dimensions())?;
if !expected_types.contains(&type_str) {
return Err(GltfError::InvalidGltf(format!(
"expected one of {expected_types:?} accessor, got {type_str}"
)));
}
let gl_enum = accessor.data_type().as_gl_enum();
if !allowed_component_types.contains(&gl_enum) {
return Err(GltfError::Unsupported(format!(
"unsupported {type_str} component type {gl_enum}"
)));
}
let num_components = accessor.dimensions().multiplicity() as u8;
let row = num_components as usize * accessor.data_type().size();
let bytes = self.extract(&accessor, row)?;
Ok(DecodedAccessor::new(
accessor.count(),
num_components,
draco_data_type(accessor.data_type()),
accessor.normalized(),
bytes,
))
}
fn read_indices(&self, accessor_idx: usize) -> std::result::Result<Vec<u32>, GltfError> {
use gltf::accessor::DataType as G;
let accessor = self.accessor(accessor_idx)?;
if !matches!(accessor.dimensions(), gltf::accessor::Dimensions::Scalar) {
return Err(GltfError::InvalidGltf(
"indices accessor must be SCALAR".into(),
));
}
let bytes = self.extract(&accessor, accessor.data_type().size())?;
let indices = match accessor.data_type() {
G::U8 => bytes.iter().map(|&b| b as u32).collect(),
G::U16 => bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]) as u32)
.collect(),
G::U32 => bytes
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
other => {
return Err(GltfError::Unsupported(format!(
"unsupported index component type {other:?}"
)))
}
};
Ok(indices)
}
}
fn dimensions_str(d: gltf::accessor::Dimensions) -> std::result::Result<&'static str, GltfError> {
use gltf::accessor::Dimensions::*;
Ok(match d {
Scalar => "SCALAR",
Vec2 => "VEC2",
Vec3 => "VEC3",
Vec4 => "VEC4",
_ => {
return Err(GltfError::Unsupported(
"matrix accessor not supported".into(),
))
}
})
}
fn draco_data_type(d: gltf::accessor::DataType) -> draco_core::draco_types::DataType {
use draco_core::draco_types::DataType as D;
use gltf::accessor::DataType as G;
match d {
G::I8 => D::Int8,
G::U8 => D::Uint8,
G::I16 => D::Int16,
G::U16 => D::Uint16,
G::U32 => D::Uint32,
G::F32 => D::Float32,
}
}
fn embed_single_buffer(doc: &mut Value, bin: &[u8]) {
if bin.is_empty() {
return;
}
if let Some(buffer) = doc
.get_mut("buffers")
.and_then(Value::as_array_mut)
.and_then(|b| b.get_mut(0))
.and_then(Value::as_object_mut)
{
buffer.insert(
"uri".into(),
Value::from(format!(
"data:application/octet-stream;base64,{}",
base64_encode(bin)
)),
);
}
}
fn base64_encode(data: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(T[(n >> 18 & 63) as usize] as char);
out.push(T[(n >> 12 & 63) as usize] as char);
out.push(if chunk.len() > 1 {
T[(n >> 6 & 63) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
T[(n & 63) as usize] as char
} else {
'='
});
}
out
}