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 a document parser: 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//! Scene-facing crates can reuse the same decode logic with only their chosen
10//! document model and resource loader.
11
12use crate::gltf_error::{GltfError, Result};
13use draco_core::draco_types::DataType;
14use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
15use draco_core::mesh::Mesh;
16
17pub(crate) const GLTF_MODE_POINTS: u32 = 0;
18pub(crate) const GLTF_MODE_TRIANGLES: u32 = 4;
19pub(crate) const GLTF_COMPONENT_BYTE: u32 = 5120;
20pub(crate) const GLTF_COMPONENT_UNSIGNED_BYTE: u32 = 5121;
21pub(crate) const GLTF_COMPONENT_SHORT: u32 = 5122;
22pub(crate) const GLTF_COMPONENT_UNSIGNED_SHORT: u32 = 5123;
23// Index component type used by accessor decoding.
24pub(crate) const GLTF_COMPONENT_UNSIGNED_INT: u32 = 5125;
25pub(crate) const GLTF_COMPONENT_FLOAT: u32 = 5126;
26
27/// One glTF accessor decoded to a tight, row-major byte block plus its layout.
28///
29/// This is the reader-agnostic unit the geometry decoder consumes: an
30/// [`AccessorSource`] produces these, and [`decode_geometry`] turns them into a
31/// [`Mesh`]. It carries the *original* component type / normalized flag, so the
32/// output accessors (which the compressor preserves) keep matching layout.
33pub struct DecodedAccessor {
34    count: usize,
35    num_components: u8,
36    data_type: DataType,
37    normalized: bool,
38    bytes: Vec<u8>,
39}
40
41impl DecodedAccessor {
42    /// Builds a decoded accessor from already-extracted values. `bytes` must be
43    /// `count * num_components * data_type.byte_length()` long, row-major.
44    pub fn new(
45        count: usize,
46        num_components: u8,
47        data_type: DataType,
48        normalized: bool,
49        bytes: Vec<u8>,
50    ) -> Result<Self> {
51        let expected = count
52            .checked_mul(num_components as usize)
53            .and_then(|count| count.checked_mul(data_type.byte_length()))
54            .ok_or_else(|| GltfError::InvalidGltf("accessor byte size overflow".into()))?;
55        if bytes.len() != expected {
56            return Err(GltfError::InvalidGltf(format!(
57                "accessor has {} bytes, expected {expected}",
58                bytes.len()
59            )));
60        }
61        Ok(Self {
62            count,
63            num_components,
64            data_type,
65            normalized,
66            bytes,
67        })
68    }
69
70    fn gather(&self, indices: &[u32]) -> Result<Self> {
71        let stride = (self.num_components as usize)
72            .checked_mul(self.data_type.byte_length())
73            .ok_or_else(|| GltfError::InvalidGltf("accessor stride overflow".into()))?;
74        let byte_len = indices
75            .len()
76            .checked_mul(stride)
77            .ok_or_else(|| GltfError::InvalidGltf("gathered accessor size overflow".into()))?;
78        let mut bytes = Vec::new();
79        bytes.try_reserve_exact(byte_len).map_err(|_| {
80            GltfError::ResourceLimitExceeded("gathered accessor allocation failed".into())
81        })?;
82
83        for &index in indices {
84            let index = index as usize;
85            if index >= self.count {
86                return Err(GltfError::InvalidGltf(format!(
87                    "Accessor index {} out of bounds for {} values",
88                    index, self.count
89                )));
90            }
91            let offset = index
92                .checked_mul(stride)
93                .ok_or_else(|| GltfError::InvalidGltf("accessor offset overflow".into()))?;
94            let end = offset
95                .checked_add(stride)
96                .filter(|end| *end <= self.bytes.len())
97                .ok_or_else(|| GltfError::InvalidGltf("accessor bytes are truncated".into()))?;
98            bytes.extend_from_slice(&self.bytes[offset..end]);
99        }
100
101        Ok(Self {
102            count: indices.len(),
103            num_components: self.num_components,
104            data_type: self.data_type,
105            normalized: self.normalized,
106            bytes,
107        })
108    }
109}
110
111/// Source of raw accessor data for [`decode_geometry`].
112///
113/// This is the seam that lets the geometry decoder run against different glTF
114/// front ends: `draco-io`'s own accessor reader implements it over the parsed
115/// glTF document, but a caller that already holds a parsed scene (e.g. a
116/// document model) can implement it over that instead and reuse the exact same
117/// decode logic.
118///
119/// Implementors only have to locate and copy out bytes; all of the geometry
120/// model building (faces, deduplication, attribute typing, multi-set semantics)
121/// lives once in [`decode_geometry`].
122pub trait AccessorSource {
123    /// Reads one attribute accessor, validating its glTF type against
124    /// `expected_types` (e.g. `["VEC3"]`) and component type against
125    /// `allowed_component_types` (glTF component-type constants).
126    fn read_attribute(
127        &self,
128        accessor: usize,
129        expected_types: &[&str],
130        allowed_component_types: &[u32],
131    ) -> Result<DecodedAccessor>;
132
133    /// Reads a `SCALAR` index accessor as `u32` values.
134    fn read_indices(&self, accessor: usize) -> Result<Vec<u32>>;
135}
136
137#[derive(Clone, Copy)]
138pub(crate) struct SemanticSpec {
139    pub(crate) attribute_type: GeometryAttributeType,
140    pub(crate) expected_accessor_types: &'static [&'static str],
141    pub(crate) allowed_component_types: &'static [u32],
142    normalization: NormalizationPolicy,
143}
144
145#[derive(Clone, Copy)]
146enum NormalizationPolicy {
147    Forbidden,
148    RequiredForInteger,
149    Generic,
150}
151
152const FLOAT_ONLY: &[u32] = &[GLTF_COMPONENT_FLOAT];
153const TEXCOORD_COMPONENT_TYPES: &[u32] = &[
154    GLTF_COMPONENT_FLOAT,
155    GLTF_COMPONENT_UNSIGNED_BYTE,
156    GLTF_COMPONENT_UNSIGNED_SHORT,
157];
158const COLOR_COMPONENT_TYPES: &[u32] = &[
159    GLTF_COMPONENT_FLOAT,
160    GLTF_COMPONENT_UNSIGNED_BYTE,
161    GLTF_COMPONENT_UNSIGNED_SHORT,
162];
163const JOINT_COMPONENT_TYPES: &[u32] =
164    &[GLTF_COMPONENT_UNSIGNED_BYTE, GLTF_COMPONENT_UNSIGNED_SHORT];
165const WEIGHT_COMPONENT_TYPES: &[u32] = &[
166    GLTF_COMPONENT_FLOAT,
167    GLTF_COMPONENT_UNSIGNED_BYTE,
168    GLTF_COMPONENT_UNSIGNED_SHORT,
169];
170const GENERIC_COMPONENT_TYPES: &[u32] = &[
171    GLTF_COMPONENT_BYTE,
172    GLTF_COMPONENT_UNSIGNED_BYTE,
173    GLTF_COMPONENT_SHORT,
174    GLTF_COMPONENT_UNSIGNED_SHORT,
175    GLTF_COMPONENT_FLOAT,
176];
177
178/// Decodes a non-Draco primitive's geometry into a [`Mesh`] plus its
179/// `(glTF semantic, Draco unique id)` mapping, reading attribute and index data
180/// through any [`AccessorSource`].
181///
182/// `mode` is the glTF primitive mode (only `POINTS` = 0 and `TRIANGLES` = 4 are
183/// supported), `attributes` maps each glTF semantic to its accessor index, and
184/// `indices` is the optional index accessor. This is the single place the
185/// geometry model is built — faces, deduplication, attribute typing, multi-set
186/// `TEXCOORD_n`/`COLOR_n`, `TANGENT`/`JOINTS_n`/`WEIGHTS_n`, and custom `_*`
187/// attributes — so different glTF front ends share it by implementing only
188/// [`AccessorSource`], never duplicating this logic.
189///
190/// The returned attribute ids equal the Draco unique ids referenced by the
191/// `KHR_draco_mesh_compression` attributes map.
192pub fn decode_geometry<S: AccessorSource>(
193    src: &S,
194    mode: u32,
195    attributes: &[(String, usize)],
196    indices: Option<usize>,
197) -> Result<(Mesh, Vec<(String, u32)>)> {
198    if mode != GLTF_MODE_TRIANGLES && mode != GLTF_MODE_POINTS {
199        return Err(GltfError::Unsupported(format!(
200            "Primitive mode {} not supported (only POINTS=0 and TRIANGLES=4)",
201            mode
202        )));
203    }
204
205    // POSITION is required.
206    let pos_accessor_idx = attributes
207        .iter()
208        .find(|(semantic, _)| semantic == "POSITION")
209        .map(|(_, accessor)| *accessor)
210        .ok_or_else(|| GltfError::InvalidGltf("primitive has no POSITION attribute".into()))?;
211
212    let positions = src.read_attribute(pos_accessor_idx, &["VEC3"], &[GLTF_COMPONENT_FLOAT])?;
213    validate_decoded_semantic("POSITION", &positions)?;
214
215    let mut mesh = Mesh::new();
216    let point_indices = if mode == GLTF_MODE_POINTS {
217        indices.map(|idx| src.read_indices(idx)).transpose()?
218    } else {
219        None
220    };
221    let positions = if let Some(idx) = &point_indices {
222        positions.gather(idx)?
223    } else {
224        positions
225    };
226    mesh.set_num_points(positions.count);
227
228    let mut semantics: Vec<(String, u32)> = Vec::new();
229    semantics.try_reserve_exact(attributes.len()).map_err(|_| {
230        GltfError::ResourceLimitExceeded("attribute semantic table allocation failed".into())
231    })?;
232    let pos_id = add_decoded_attribute(&mut mesh, GeometryAttributeType::Position, positions)?;
233    semantics.push(("POSITION".to_string(), pos_id as u32));
234
235    if mode == GLTF_MODE_TRIANGLES {
236        if let Some(indices_accessor_idx) = indices {
237            let indices = src.read_indices(indices_accessor_idx)?;
238            if indices.len() % 3 != 0 {
239                return Err(GltfError::InvalidGltf(
240                    "Index count not divisible by 3 for triangles".into(),
241                ));
242            }
243            for &index in &indices {
244                if index as usize >= mesh.num_points() {
245                    return Err(GltfError::InvalidGltf(format!(
246                        "Triangle index {} out of bounds for {} points",
247                        index,
248                        mesh.num_points()
249                    )));
250                }
251            }
252            let num_faces = indices.len() / 3;
253            mesh.try_set_num_faces(num_faces)
254                .map_err(GltfError::DracoEncode)?;
255            for (face_id, face) in indices.chunks_exact(3).enumerate() {
256                mesh.set_face_from_indices(face_id, [face[0], face[1], face[2]]);
257            }
258        } else {
259            // Non-indexed: generate sequential triangle faces.
260            if !mesh.num_points().is_multiple_of(3) {
261                return Err(GltfError::InvalidGltf(
262                    "Non-indexed primitive point count not divisible by 3".into(),
263                ));
264            }
265            let num_faces = mesh.num_points() / 3;
266            mesh.try_set_num_faces(num_faces)
267                .map_err(GltfError::DracoEncode)?;
268            for face_id in 0..num_faces {
269                let base = face_id
270                    .checked_mul(3)
271                    .and_then(|base| u32::try_from(base).ok())
272                    .ok_or_else(|| {
273                        GltfError::InvalidGltf(
274                            "Non-indexed primitive exceeds Draco's u32 point-id limit".into(),
275                        )
276                    })?;
277                let second = base
278                    .checked_add(1)
279                    .ok_or_else(|| GltfError::InvalidGltf("Triangle point-id overflow".into()))?;
280                let third = base
281                    .checked_add(2)
282                    .ok_or_else(|| GltfError::InvalidGltf("Triangle point-id overflow".into()))?;
283                mesh.set_face_from_indices(face_id, [base, second, third]);
284            }
285        }
286    }
287
288    // Optionally read NORMAL.
289    if let Some(normal_idx) = attributes
290        .iter()
291        .find(|(semantic, _)| semantic == "NORMAL")
292        .map(|(_, accessor)| *accessor)
293    {
294        let spec = supported_semantic_spec("NORMAL")?;
295        let normal_id = read_and_add_standard_attribute(
296            &mut mesh,
297            src,
298            normal_idx,
299            "NORMAL",
300            spec,
301            point_indices.as_deref(),
302        )?;
303        semantics.push(("NORMAL".to_string(), normal_id as u32));
304    }
305
306    // Read every remaining semantic in sorted order. Draco can carry multiple
307    // attributes with the same semantic type (extra TEXCOORD_n/COLOR_n), plus
308    // TANGENT, JOINTS_n, WEIGHTS_n, and custom `_*`.
309    let mut sorted: Vec<&(String, usize)> = Vec::new();
310    sorted.try_reserve_exact(attributes.len()).map_err(|_| {
311        GltfError::ResourceLimitExceeded("attribute sort table allocation failed".into())
312    })?;
313    sorted.extend(attributes.iter());
314    sorted.sort_by(|(left, _), (right, _)| left.cmp(right));
315    for (semantic, accessor_idx) in sorted {
316        if semantic == "POSITION" || semantic == "NORMAL" {
317            continue;
318        }
319        let attribute_spec = supported_semantic_spec(semantic)?;
320        let att_id = read_and_add_standard_attribute(
321            &mut mesh,
322            src,
323            *accessor_idx,
324            semantic,
325            attribute_spec,
326            point_indices.as_deref(),
327        )?;
328        semantics.push((semantic.clone(), att_id as u32));
329    }
330
331    // Match C++ Draco: deduplicate point IDs in face-traversal order for binary
332    // compatibility. Remapping does not change attribute ids, so `semantics`
333    // stays valid. (Draco-compressed meshes don't need this.)
334    mesh.deduplicate_point_ids();
335
336    Ok((mesh, semantics))
337}
338
339fn read_and_add_standard_attribute<S: AccessorSource>(
340    mesh: &mut Mesh,
341    src: &S,
342    accessor_idx: usize,
343    semantic: &str,
344    spec: SemanticSpec,
345    point_indices: Option<&[u32]>,
346) -> Result<i32> {
347    let decoded = src.read_attribute(
348        accessor_idx,
349        spec.expected_accessor_types,
350        spec.allowed_component_types,
351    )?;
352    validate_decoded_semantic(semantic, &decoded)?;
353    let decoded = if let Some(indices) = point_indices {
354        decoded.gather(indices)?
355    } else {
356        decoded
357    };
358    add_decoded_attribute(mesh, spec.attribute_type, decoded)
359}
360
361/// Adds a decoded attribute to the mesh, returning the new attribute id (which
362/// equals its Draco unique id).
363fn add_decoded_attribute(
364    mesh: &mut Mesh,
365    attribute_type: GeometryAttributeType,
366    decoded: DecodedAccessor,
367) -> Result<i32> {
368    if decoded.count != mesh.num_points() {
369        return Err(GltfError::InvalidGltf(format!(
370            "Attribute {:?} has {} values but primitive has {} points",
371            attribute_type,
372            decoded.count,
373            mesh.num_points()
374        )));
375    }
376
377    let mut attribute = PointAttribute::new();
378    attribute
379        .try_init(
380            attribute_type,
381            decoded.num_components,
382            decoded.data_type,
383            decoded.normalized,
384            decoded.count,
385        )
386        .map_err(GltfError::DracoEncode)?;
387    if !attribute.buffer_mut().try_write(0, &decoded.bytes) {
388        return Err(GltfError::DracoEncode(draco_core::DracoError::BufferError(
389            "Decoded glTF attribute does not fit its Draco buffer".into(),
390        )));
391    }
392    Ok(mesh.add_attribute(attribute))
393}
394
395pub(crate) fn supported_semantic_spec(semantic: &str) -> Result<SemanticSpec> {
396    let spec = if semantic == "POSITION" {
397        SemanticSpec {
398            attribute_type: GeometryAttributeType::Position,
399            expected_accessor_types: &["VEC3"],
400            allowed_component_types: FLOAT_ONLY,
401            normalization: NormalizationPolicy::Forbidden,
402        }
403    } else if semantic == "NORMAL" {
404        SemanticSpec {
405            attribute_type: GeometryAttributeType::Normal,
406            expected_accessor_types: &["VEC3"],
407            allowed_component_types: FLOAT_ONLY,
408            normalization: NormalizationPolicy::Forbidden,
409        }
410    } else if semantic == "TANGENT" {
411        SemanticSpec {
412            attribute_type: GeometryAttributeType::Generic,
413            expected_accessor_types: &["VEC4"],
414            allowed_component_types: FLOAT_ONLY,
415            normalization: NormalizationPolicy::Forbidden,
416        }
417    } else if indexed_semantic(semantic, "TEXCOORD_") {
418        SemanticSpec {
419            attribute_type: GeometryAttributeType::TexCoord,
420            expected_accessor_types: &["VEC2"],
421            allowed_component_types: TEXCOORD_COMPONENT_TYPES,
422            normalization: NormalizationPolicy::RequiredForInteger,
423        }
424    } else if indexed_semantic(semantic, "COLOR_") {
425        SemanticSpec {
426            attribute_type: GeometryAttributeType::Color,
427            expected_accessor_types: &["VEC3", "VEC4"],
428            allowed_component_types: COLOR_COMPONENT_TYPES,
429            normalization: NormalizationPolicy::RequiredForInteger,
430        }
431    } else if indexed_semantic(semantic, "JOINTS_") {
432        SemanticSpec {
433            attribute_type: GeometryAttributeType::Generic,
434            expected_accessor_types: &["VEC4"],
435            allowed_component_types: JOINT_COMPONENT_TYPES,
436            normalization: NormalizationPolicy::Forbidden,
437        }
438    } else if indexed_semantic(semantic, "WEIGHTS_") {
439        SemanticSpec {
440            attribute_type: GeometryAttributeType::Generic,
441            expected_accessor_types: &["VEC4"],
442            allowed_component_types: WEIGHT_COMPONENT_TYPES,
443            normalization: NormalizationPolicy::RequiredForInteger,
444        }
445    } else if semantic.starts_with('_') && semantic.len() > 1 {
446        // Application-specific semantics are carried as generic Draco
447        // attributes. The semantic name remains in primitive.attributes and in
448        // the KHR_draco_mesh_compression attribute map.
449        SemanticSpec {
450            attribute_type: GeometryAttributeType::Generic,
451            expected_accessor_types: &["SCALAR", "VEC2", "VEC3", "VEC4"],
452            allowed_component_types: GENERIC_COMPONENT_TYPES,
453            normalization: NormalizationPolicy::Generic,
454        }
455    } else {
456        return Err(GltfError::InvalidGltf(format!(
457            "invalid glTF attribute semantic {semantic}"
458        )));
459    };
460
461    Ok(spec)
462}
463
464fn indexed_semantic(semantic: &str, prefix: &str) -> bool {
465    semantic
466        .strip_prefix(prefix)
467        .is_some_and(|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
468}
469
470pub(crate) fn validate_semantic_accessor(
471    semantic: &str,
472    accessor_type: &str,
473    component_type: u32,
474    normalized: bool,
475) -> Result<SemanticSpec> {
476    let spec = supported_semantic_spec(semantic)?;
477    if !spec.expected_accessor_types.contains(&accessor_type) {
478        return Err(GltfError::InvalidGltf(format!(
479            "{semantic} accessor type {accessor_type} is invalid"
480        )));
481    }
482    if !spec.allowed_component_types.contains(&component_type) {
483        return Err(GltfError::InvalidGltf(format!(
484            "{semantic} accessor componentType {component_type} is invalid"
485        )));
486    }
487    let integer = component_type != GLTF_COMPONENT_FLOAT;
488    let valid_normalized = match spec.normalization {
489        NormalizationPolicy::Forbidden => !normalized,
490        NormalizationPolicy::RequiredForInteger => normalized == integer,
491        NormalizationPolicy::Generic => !normalized || integer,
492    };
493    if !valid_normalized {
494        return Err(GltfError::InvalidGltf(format!(
495            "{semantic} accessor normalized={normalized} is invalid for componentType {component_type}"
496        )));
497    }
498    Ok(spec)
499}
500
501fn validate_decoded_semantic(semantic: &str, accessor: &DecodedAccessor) -> Result<()> {
502    validate_semantic_accessor(
503        semantic,
504        gltf_type_for_num_components(accessor.num_components)?,
505        component_type_for_data_type(accessor.data_type)?,
506        accessor.normalized,
507    )?;
508    Ok(())
509}
510
511pub(crate) fn gltf_type_for_num_components(num_components: u8) -> Result<&'static str> {
512    match num_components {
513        1 => Ok("SCALAR"),
514        2 => Ok("VEC2"),
515        3 => Ok("VEC3"),
516        4 => Ok("VEC4"),
517        _ => Err(GltfError::InvalidGltf(format!(
518            "Invalid accessor component count: {num_components}"
519        ))),
520    }
521}
522
523pub(crate) fn component_type_for_data_type(data_type: DataType) -> Result<u32> {
524    match data_type {
525        DataType::Int8 => Ok(GLTF_COMPONENT_BYTE),
526        DataType::Uint8 => Ok(GLTF_COMPONENT_UNSIGNED_BYTE),
527        DataType::Int16 => Ok(GLTF_COMPONENT_SHORT),
528        DataType::Uint16 => Ok(GLTF_COMPONENT_UNSIGNED_SHORT),
529        DataType::Uint32 => Ok(GLTF_COMPONENT_UNSIGNED_INT),
530        DataType::Float32 => Ok(GLTF_COMPONENT_FLOAT),
531        _ => Err(GltfError::Unsupported(format!(
532            "Unsupported Draco attribute data type for glTF: {data_type:?}"
533        ))),
534    }
535}