Skip to main content

draco_io/
gltf_geometry.rs

1//! Reader-agnostic glTF geometry decoding.
2//!
3//! This module holds the parts of glTF geometry handling that do **not** depend
4//! on `draco-io`'s own glTF reader: the shared error type, the
5//! [`AccessorSource`] seam, and [`decode_geometry`], which builds a
6//! [`draco_core::Mesh`] (faces, deduplication, attribute typing, multi-set
7//! semantics) from whatever accessor data a source yields.
8//!
9//! It is compiled whenever the glTF reader **or** writer is enabled, so the
10//! document-preserving compressor ([`crate::compress_gltf_value`]) and external
11//! front ends (e.g. a `gltf-rs` document) can reuse the same decode logic with
12//! only the encoder, never linking the reader.
13
14use std::io;
15
16use draco_core::draco_types::DataType;
17use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
18use draco_core::mesh::Mesh;
19use thiserror::Error;
20
21/// Errors that can occur when reading or decoding glTF geometry.
22#[derive(Error, Debug)]
23pub enum GltfError {
24    /// Filesystem or stream I/O failed.
25    #[error("IO error: {0}")]
26    Io(#[from] io::Error),
27
28    /// glTF JSON parsing failed.
29    #[error("JSON parse error: {0}")]
30    Json(#[from] serde_json::Error),
31
32    /// Binary GLB structure is invalid.
33    #[error("Invalid GLB: {0}")]
34    InvalidGlb(String),
35
36    /// glTF JSON or accessor/buffer structure is invalid.
37    #[error("Invalid glTF: {0}")]
38    InvalidGltf(String),
39
40    /// Embedded Draco payload failed to decode.
41    #[error("Draco decode error: {0}")]
42    DracoDecode(String),
43
44    /// The asset uses a glTF feature outside this crate's geometry scope.
45    #[error("Unsupported feature: {0}")]
46    Unsupported(String),
47}
48
49/// Result type used by glTF readers and the geometry decoder.
50pub type Result<T> = std::result::Result<T, GltfError>;
51
52pub(crate) const GLTF_MODE_POINTS: u32 = 0;
53pub(crate) const GLTF_MODE_TRIANGLES: u32 = 4;
54pub(crate) const GLTF_COMPONENT_BYTE: u32 = 5120;
55pub(crate) const GLTF_COMPONENT_UNSIGNED_BYTE: u32 = 5121;
56pub(crate) const GLTF_COMPONENT_SHORT: u32 = 5122;
57pub(crate) const GLTF_COMPONENT_UNSIGNED_SHORT: u32 = 5123;
58// Index component type, used only by the reader's index decoding.
59#[cfg(feature = "gltf-reader")]
60pub(crate) const GLTF_COMPONENT_UNSIGNED_INT: u32 = 5125;
61pub(crate) const GLTF_COMPONENT_FLOAT: u32 = 5126;
62
63/// One glTF accessor decoded to a tight, row-major byte block plus its layout.
64///
65/// This is the reader-agnostic unit the geometry decoder consumes: an
66/// [`AccessorSource`] produces these, and [`decode_geometry`] turns them into a
67/// [`Mesh`]. It carries the *original* component type / normalized flag, so the
68/// output accessors (which the compressor preserves) keep matching layout.
69pub struct DecodedAccessor {
70    count: usize,
71    num_components: u8,
72    data_type: DataType,
73    normalized: bool,
74    bytes: Vec<u8>,
75}
76
77impl DecodedAccessor {
78    /// Builds a decoded accessor from already-extracted values. `bytes` must be
79    /// `count * num_components * data_type.byte_length()` long, row-major.
80    pub fn new(
81        count: usize,
82        num_components: u8,
83        data_type: DataType,
84        normalized: bool,
85        bytes: Vec<u8>,
86    ) -> Self {
87        Self {
88            count,
89            num_components,
90            data_type,
91            normalized,
92            bytes,
93        }
94    }
95
96    fn gather(&self, indices: &[u32]) -> Result<Self> {
97        let stride = self.num_components as usize * self.data_type.byte_length();
98        let mut bytes = Vec::with_capacity(indices.len() * stride);
99
100        for &index in indices {
101            let index = index as usize;
102            if index >= self.count {
103                return Err(GltfError::InvalidGltf(format!(
104                    "Accessor index {} out of bounds for {} values",
105                    index, self.count
106                )));
107            }
108            let offset = index * stride;
109            bytes.extend_from_slice(&self.bytes[offset..offset + stride]);
110        }
111
112        Ok(Self {
113            count: indices.len(),
114            num_components: self.num_components,
115            data_type: self.data_type,
116            normalized: self.normalized,
117            bytes,
118        })
119    }
120}
121
122/// Source of raw accessor data for [`decode_geometry`].
123///
124/// This is the seam that lets the geometry decoder run against different glTF
125/// front ends: `draco-io`'s own accessor reader implements it over the parsed
126/// glTF document, but a caller that already holds a parsed scene (e.g. a
127/// `gltf-rs` document) can implement it over that instead and reuse the exact
128/// same decode logic, without linking `draco-io`'s glTF reader.
129///
130/// Implementors only have to locate and copy out bytes; all of the geometry
131/// model building (faces, deduplication, attribute typing, multi-set semantics)
132/// lives once in [`decode_geometry`].
133pub trait AccessorSource {
134    /// Reads one attribute accessor, validating its glTF type against
135    /// `expected_types` (e.g. `["VEC3"]`) and component type against
136    /// `allowed_component_types` (glTF component-type constants).
137    fn read_attribute(
138        &self,
139        accessor: usize,
140        expected_types: &[&str],
141        allowed_component_types: &[u32],
142    ) -> Result<DecodedAccessor>;
143
144    /// Reads a `SCALAR` index accessor as `u32` values.
145    fn read_indices(&self, accessor: usize) -> Result<Vec<u32>>;
146}
147
148pub(crate) struct SemanticSpec {
149    pub(crate) attribute_type: GeometryAttributeType,
150    pub(crate) expected_accessor_types: &'static [&'static str],
151    pub(crate) allowed_component_types: &'static [u32],
152}
153
154const FLOAT_ONLY: &[u32] = &[GLTF_COMPONENT_FLOAT];
155const TEXCOORD_COMPONENT_TYPES: &[u32] = &[
156    GLTF_COMPONENT_FLOAT,
157    GLTF_COMPONENT_UNSIGNED_BYTE,
158    GLTF_COMPONENT_UNSIGNED_SHORT,
159];
160const COLOR_COMPONENT_TYPES: &[u32] = &[
161    GLTF_COMPONENT_FLOAT,
162    GLTF_COMPONENT_UNSIGNED_BYTE,
163    GLTF_COMPONENT_UNSIGNED_SHORT,
164];
165const GENERIC_COMPONENT_TYPES: &[u32] = &[
166    GLTF_COMPONENT_BYTE,
167    GLTF_COMPONENT_UNSIGNED_BYTE,
168    GLTF_COMPONENT_SHORT,
169    GLTF_COMPONENT_UNSIGNED_SHORT,
170    GLTF_COMPONENT_FLOAT,
171];
172
173/// Decodes a non-Draco primitive's geometry into a [`Mesh`] plus its
174/// `(glTF semantic, Draco unique id)` mapping, reading attribute and index data
175/// through any [`AccessorSource`].
176///
177/// `mode` is the glTF primitive mode (only `POINTS` = 0 and `TRIANGLES` = 4 are
178/// supported), `attributes` maps each glTF semantic to its accessor index, and
179/// `indices` is the optional index accessor. This is the single place the
180/// geometry model is built — faces, deduplication, attribute typing, multi-set
181/// `TEXCOORD_n`/`COLOR_n`, `TANGENT`/`JOINTS_n`/`WEIGHTS_n`, and custom `_*`
182/// attributes — so different glTF front ends share it by implementing only
183/// [`AccessorSource`], never duplicating this logic.
184///
185/// The returned attribute ids equal the Draco unique ids referenced by the
186/// `KHR_draco_mesh_compression` attributes map.
187pub fn decode_geometry<S: AccessorSource>(
188    src: &S,
189    mode: u32,
190    attributes: &[(String, usize)],
191    indices: Option<usize>,
192) -> Result<(Mesh, Vec<(String, u32)>)> {
193    use draco_core::geometry_indices::PointIndex;
194
195    if mode != GLTF_MODE_TRIANGLES && mode != GLTF_MODE_POINTS {
196        return Err(GltfError::Unsupported(format!(
197            "Primitive mode {} not supported (only POINTS=0 and TRIANGLES=4)",
198            mode
199        )));
200    }
201
202    // POSITION is required.
203    let pos_accessor_idx = attributes
204        .iter()
205        .find(|(semantic, _)| semantic == "POSITION")
206        .map(|(_, accessor)| *accessor)
207        .ok_or_else(|| GltfError::InvalidGltf("primitive has no POSITION attribute".into()))?;
208
209    let positions = src.read_attribute(pos_accessor_idx, &["VEC3"], &[GLTF_COMPONENT_FLOAT])?;
210
211    let mut mesh = Mesh::new();
212    let point_indices = if mode == GLTF_MODE_POINTS {
213        indices.map(|idx| src.read_indices(idx)).transpose()?
214    } else {
215        None
216    };
217    let positions = if let Some(idx) = &point_indices {
218        positions.gather(idx)?
219    } else {
220        positions
221    };
222    mesh.set_num_points(positions.count);
223
224    let mut semantics: Vec<(String, u32)> = Vec::new();
225    let pos_id = add_decoded_attribute(&mut mesh, GeometryAttributeType::Position, positions)?;
226    semantics.push(("POSITION".to_string(), pos_id as u32));
227
228    if mode == GLTF_MODE_TRIANGLES {
229        if let Some(indices_accessor_idx) = indices {
230            let indices = src.read_indices(indices_accessor_idx)?;
231            if indices.len() % 3 != 0 {
232                return Err(GltfError::InvalidGltf(
233                    "Index count not divisible by 3 for triangles".into(),
234                ));
235            }
236            for &index in &indices {
237                if index as usize >= mesh.num_points() {
238                    return Err(GltfError::InvalidGltf(format!(
239                        "Triangle index {} out of bounds for {} points",
240                        index,
241                        mesh.num_points()
242                    )));
243                }
244            }
245            let num_faces = indices.len() / 3;
246            for i in 0..num_faces {
247                mesh.add_face([
248                    PointIndex(indices[i * 3]),
249                    PointIndex(indices[i * 3 + 1]),
250                    PointIndex(indices[i * 3 + 2]),
251                ]);
252            }
253        } else {
254            // Non-indexed: generate sequential triangle faces.
255            if !mesh.num_points().is_multiple_of(3) {
256                return Err(GltfError::InvalidGltf(
257                    "Non-indexed primitive point count not divisible by 3".into(),
258                ));
259            }
260            for i in 0..(mesh.num_points() / 3) {
261                let base = (i * 3) as u32;
262                mesh.add_face([PointIndex(base), PointIndex(base + 1), PointIndex(base + 2)]);
263            }
264        }
265    }
266
267    // Optionally read NORMAL.
268    if let Some(normal_idx) = attributes
269        .iter()
270        .find(|(semantic, _)| semantic == "NORMAL")
271        .map(|(_, accessor)| *accessor)
272    {
273        let normal_id = read_and_add_standard_attribute(
274            &mut mesh,
275            src,
276            normal_idx,
277            GeometryAttributeType::Normal,
278            &["VEC3"],
279            &[GLTF_COMPONENT_FLOAT],
280            point_indices.as_deref(),
281        )?;
282        semantics.push(("NORMAL".to_string(), normal_id as u32));
283    }
284
285    // Read every remaining semantic in sorted order. Draco can carry multiple
286    // attributes with the same semantic type (extra TEXCOORD_n/COLOR_n), plus
287    // TANGENT, JOINTS_n, WEIGHTS_n, and custom `_*`.
288    let mut sorted: Vec<&(String, usize)> = attributes.iter().collect();
289    sorted.sort_by(|(left, _), (right, _)| left.cmp(right));
290    for (semantic, accessor_idx) in sorted {
291        if semantic == "POSITION" || semantic == "NORMAL" {
292            continue;
293        }
294        let attribute_spec = supported_semantic_spec(semantic)?;
295        let att_id = read_and_add_standard_attribute(
296            &mut mesh,
297            src,
298            *accessor_idx,
299            attribute_spec.attribute_type,
300            attribute_spec.expected_accessor_types,
301            attribute_spec.allowed_component_types,
302            point_indices.as_deref(),
303        )?;
304        semantics.push((semantic.clone(), att_id as u32));
305    }
306
307    // Match C++ Draco: deduplicate point IDs in face-traversal order for binary
308    // compatibility. Remapping does not change attribute ids, so `semantics`
309    // stays valid. (Draco-compressed meshes don't need this.)
310    mesh.deduplicate_point_ids();
311
312    Ok((mesh, semantics))
313}
314
315/// Reads the attribute for `semantic` from `src` and adds it to `mesh`,
316/// returning its attribute id. Used by the reader for side attributes that
317/// accompany a Draco stream but are not carried inside it.
318#[cfg(feature = "gltf-reader")]
319pub(crate) fn add_named_attribute<S: AccessorSource>(
320    mesh: &mut Mesh,
321    src: &S,
322    semantic: &str,
323    accessor_idx: usize,
324    point_indices: Option<&[u32]>,
325) -> Result<i32> {
326    let spec = supported_semantic_spec(semantic)?;
327    read_and_add_standard_attribute(
328        mesh,
329        src,
330        accessor_idx,
331        spec.attribute_type,
332        spec.expected_accessor_types,
333        spec.allowed_component_types,
334        point_indices,
335    )
336}
337
338fn read_and_add_standard_attribute<S: AccessorSource>(
339    mesh: &mut Mesh,
340    src: &S,
341    accessor_idx: usize,
342    attribute_type: GeometryAttributeType,
343    expected_types: &[&str],
344    allowed_component_types: &[u32],
345    point_indices: Option<&[u32]>,
346) -> Result<i32> {
347    let decoded = src.read_attribute(accessor_idx, expected_types, allowed_component_types)?;
348    let decoded = if let Some(indices) = point_indices {
349        decoded.gather(indices)?
350    } else {
351        decoded
352    };
353    add_decoded_attribute(mesh, attribute_type, decoded)
354}
355
356/// Adds a decoded attribute to the mesh, returning the new attribute id (which
357/// equals its Draco unique id).
358fn add_decoded_attribute(
359    mesh: &mut Mesh,
360    attribute_type: GeometryAttributeType,
361    decoded: DecodedAccessor,
362) -> Result<i32> {
363    if decoded.count != mesh.num_points() {
364        return Err(GltfError::InvalidGltf(format!(
365            "Attribute {:?} has {} values but primitive has {} points",
366            attribute_type,
367            decoded.count,
368            mesh.num_points()
369        )));
370    }
371
372    let mut attribute = PointAttribute::new();
373    attribute.init(
374        attribute_type,
375        decoded.num_components,
376        decoded.data_type,
377        decoded.normalized,
378        decoded.count,
379    );
380    attribute.buffer_mut().write(0, &decoded.bytes);
381    Ok(mesh.add_attribute(attribute))
382}
383
384pub(crate) fn supported_semantic_spec(semantic: &str) -> Result<SemanticSpec> {
385    let spec = if semantic == "POSITION" {
386        SemanticSpec {
387            attribute_type: GeometryAttributeType::Position,
388            expected_accessor_types: &["VEC3"],
389            allowed_component_types: FLOAT_ONLY,
390        }
391    } else if semantic == "NORMAL" {
392        SemanticSpec {
393            attribute_type: GeometryAttributeType::Normal,
394            expected_accessor_types: &["VEC3"],
395            allowed_component_types: FLOAT_ONLY,
396        }
397    } else if semantic.starts_with("TEXCOORD_") {
398        SemanticSpec {
399            attribute_type: GeometryAttributeType::TexCoord,
400            expected_accessor_types: &["VEC2"],
401            allowed_component_types: TEXCOORD_COMPONENT_TYPES,
402        }
403    } else if semantic.starts_with("COLOR_") {
404        SemanticSpec {
405            attribute_type: GeometryAttributeType::Color,
406            expected_accessor_types: &["VEC3", "VEC4"],
407            allowed_component_types: COLOR_COMPONENT_TYPES,
408        }
409    } else {
410        // TANGENT, JOINTS_n, WEIGHTS_n, custom `_*`, and any other semantic are
411        // carried as generic attributes. The glTF semantic name is not stored on
412        // the Draco attribute itself; it is preserved by the caller through the
413        // `KHR_draco_mesh_compression` attributes map and `primitive.attributes`.
414        SemanticSpec {
415            attribute_type: GeometryAttributeType::Generic,
416            expected_accessor_types: &["SCALAR", "VEC2", "VEC3", "VEC4"],
417            allowed_component_types: GENERIC_COMPONENT_TYPES,
418        }
419    };
420
421    Ok(spec)
422}