Skip to main content

draco_oxide/io/obj/
mod.rs

1use draco_oxide_core::attribute::AttributeDomain;
2// use tobj to load the obj file and convert it to our internal mesh representation
3use draco_oxide_core::attribute::AttributeType;
4use draco_oxide_core::mesh::builder::MeshBuilder;
5use draco_oxide_core::mesh::Mesh;
6use draco_oxide_core::types::NdVector;
7use std::fmt::Debug;
8use std::path::Path;
9
10#[derive(Debug, thiserror::Error, Clone)]
11pub enum Err {
12    #[error("Mesh Builder Error: {0}")]
13    MeshBuilderError(#[from] draco_oxide_core::mesh::builder::Err),
14}
15
16pub fn load_obj<P: AsRef<Path> + Debug>(path: P) -> Result<Mesh, Err> {
17    let op = tobj::LoadOptions {
18        triangulate: true,
19        single_index: true,
20        ..Default::default()
21    };
22
23    let (models, _materials) = tobj::load_obj(path, &op).expect("Failed to load OBJ file");
24    let model: &tobj::Model = &models[0];
25    let pos = model
26        .mesh
27        .positions
28        .chunks(3)
29        .map(|x| NdVector::from([x[0], x[1], x[2]]))
30        .collect::<Vec<_>>();
31    let faces = model
32        .mesh
33        .indices
34        .chunks(3)
35        .map(|x| [x[0] as usize, x[1] as usize, x[2] as usize])
36        .collect::<Vec<_>>();
37    let (normals, normals_domain_ty) = load_normals(&model.mesh);
38    let (tex_coords, tex_coords_domain_ty) = load_tex_coords(&model.mesh);
39    let mut builder = MeshBuilder::new();
40    builder.set_connectivity_attribute(faces);
41    let pos_att_id = builder.add_attribute(
42        pos,
43        AttributeType::Position,
44        AttributeDomain::Position,
45        vec![],
46    );
47    if !normals.is_empty() {
48        builder.add_attribute(
49            normals,
50            AttributeType::Normal,
51            normals_domain_ty,
52            vec![pos_att_id],
53        );
54    }
55    if !tex_coords.is_empty() {
56        builder.add_attribute(
57            tex_coords,
58            AttributeType::TextureCoordinate,
59            tex_coords_domain_ty,
60            vec![pos_att_id],
61        );
62    }
63
64    Ok(builder.build()?)
65}
66
67fn load_normals(mesh: &tobj::Mesh) -> (Vec<NdVector<3, f32>>, AttributeDomain) {
68    if mesh.normals.is_empty() {
69        return (vec![], AttributeDomain::Position);
70    }
71    let normals = mesh
72        .normals
73        .chunks(3)
74        .map(|x| NdVector::from([x[0], x[1], x[2]]))
75        .collect::<Vec<_>>();
76    (normals, AttributeDomain::Corner)
77}
78
79fn load_tex_coords(mesh: &tobj::Mesh) -> (Vec<NdVector<2, f32>>, AttributeDomain) {
80    if mesh.texcoords.is_empty() {
81        return (vec![], AttributeDomain::Position);
82    }
83    let tex_coords = mesh
84        .texcoords
85        .chunks(2)
86        .map(|x| NdVector::from([x[0], x[1]]))
87        .collect::<Vec<_>>();
88
89    (tex_coords, AttributeDomain::Corner)
90}
91
92#[cfg(test)]
93mod tests {
94    use draco_oxide_core::types::PointIdx;
95
96    use super::*;
97
98    #[test]
99    fn tetrahedron() {
100        let mesh = load_obj("../tests/data/tetrahedron.obj").unwrap();
101        assert_eq!(
102            mesh.get_faces(),
103            vec![
104                [PointIdx::from(0), PointIdx::from(1), PointIdx::from(2)],
105                [PointIdx::from(0), PointIdx::from(3), PointIdx::from(1)],
106                [PointIdx::from(0), PointIdx::from(2), PointIdx::from(4)],
107                [PointIdx::from(1), PointIdx::from(5), PointIdx::from(2)]
108            ]
109        );
110        assert_eq!(mesh.attributes.len(), 3);
111        assert_eq!(
112            mesh.attributes[0].get_attribute_type(),
113            AttributeType::Position
114        );
115        assert_eq!(mesh.attributes[0].get_domain(), AttributeDomain::Position);
116        assert_eq!(mesh.attributes[0].get_num_components(), 3);
117        assert_eq!(mesh.attributes[0].num_unique_values(), 4);
118        assert_eq!(mesh.attributes[0].len(), 6);
119    }
120}