Skip to main content

draco_io/
obj_reader.rs

1//! OBJ format reader for meshes and point clouds.
2//!
3//! Provides both a struct-based API (`ObjReader`) and convenience functions.
4
5use std::fs;
6use std::io::{self, BufRead, BufReader, Cursor};
7use std::path::Path;
8
9use draco_core::draco_types::DataType;
10use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
11use draco_core::mesh::Mesh;
12use std::collections::HashMap;
13
14use crate::traits::{PointCloudReader, ReadFromBytes, Reader};
15
16/// OBJ format reader.
17///
18/// Reads vertex positions, texture coordinates, normals, and faces from OBJ files.
19#[derive(Debug)]
20pub struct ObjReader {
21    source: ObjReaderSource,
22}
23
24#[derive(Debug, Clone)]
25enum ObjReaderSource {
26    Path(std::path::PathBuf),
27    Bytes(Vec<u8>),
28}
29
30impl ObjReader {
31    /// Open an OBJ file for reading.
32    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
33        let path = path.as_ref().to_path_buf();
34        if !path.exists() {
35            return Err(io::Error::new(
36                io::ErrorKind::NotFound,
37                format!("File not found: {}", path.display()),
38            ));
39        }
40        Ok(Self {
41            source: ObjReaderSource::Path(path),
42        })
43    }
44
45    /// Create an OBJ reader from in-memory bytes.
46    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
47        Self {
48            source: ObjReaderSource::Bytes(bytes.into()),
49        }
50    }
51
52    /// Read a mesh directly from in-memory bytes.
53    pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Mesh> {
54        let mut reader = Self::from_bytes(bytes.to_vec());
55        reader.read_mesh()
56    }
57
58    /// Read all positions from the OBJ file.
59    pub fn read_positions(&mut self) -> io::Result<Vec<[f32; 3]>> {
60        match &self.source {
61            ObjReaderSource::Path(path) => read_obj_positions(path),
62            ObjReaderSource::Bytes(bytes) => {
63                read_obj_positions_from_reader(BufReader::new(Cursor::new(bytes.as_slice())))
64            }
65        }
66    }
67
68    /// Read mesh geometry from the OBJ file.
69    fn read_mesh_data(&self) -> io::Result<ParsedObjMesh> {
70        match &self.source {
71            ObjReaderSource::Path(path) => {
72                let file = fs::File::open(path)?;
73                read_obj_mesh_from_reader(BufReader::new(file))
74            }
75            ObjReaderSource::Bytes(bytes) => {
76                read_obj_mesh_from_reader(BufReader::new(Cursor::new(bytes.as_slice())))
77            }
78        }
79    }
80
81    /// Read a mesh with positions and faces (if present).
82    ///
83    /// The mesh is automatically processed to match C++ Draco OBJ loader behavior:
84    /// point IDs are deduplicated in face-traversal order, which ensures binary
85    /// compatibility when encoding with sequential encoding (speed 10).
86    pub fn read_mesh(&mut self) -> io::Result<Mesh> {
87        let parsed = self.read_mesh_data()?;
88        let mut mesh = Mesh::new();
89
90        if parsed.positions.is_empty() {
91            return Ok(mesh);
92        }
93
94        mesh.set_num_points(parsed.positions.len());
95        mesh.set_num_faces(parsed.faces.len());
96
97        mesh.add_attribute(make_f32x3_attribute(
98            GeometryAttributeType::Position,
99            &parsed.positions,
100        ));
101
102        if let Some(normals) = parsed.normals.as_ref() {
103            mesh.add_attribute(make_f32x3_attribute(GeometryAttributeType::Normal, normals));
104        }
105
106        if let Some(texcoords) = parsed.texcoords.as_ref() {
107            mesh.add_attribute(make_f32x2_attribute(
108                GeometryAttributeType::TexCoord,
109                texcoords,
110            ));
111        }
112
113        // Set faces
114        use draco_core::geometry_indices::{FaceIndex, PointIndex};
115        for (i, face) in parsed.faces.iter().enumerate() {
116            mesh.set_face(
117                FaceIndex(i as u32),
118                [
119                    PointIndex(face[0]),
120                    PointIndex(face[1]),
121                    PointIndex(face[2]),
122                ],
123            );
124        }
125
126        // Match C++ OBJ loader behavior: deduplicate point IDs in face-traversal order.
127        // C++ creates separate points for each face corner then deduplicates them,
128        // assigning new IDs in the order points are first encountered in faces.
129        mesh.deduplicate_point_ids();
130
131        Ok(mesh)
132    }
133}
134
135impl Reader for ObjReader {
136    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
137        ObjReader::open(path)
138    }
139
140    fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
141        let m = self.read_mesh()?;
142        Ok(vec![m])
143    }
144}
145
146impl ReadFromBytes for ObjReader {
147    fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
148        Ok(Self::from_bytes(bytes.to_vec()))
149    }
150}
151
152impl PointCloudReader for ObjReader {
153    fn read_points(&mut self) -> io::Result<Vec<[f32; 3]>> {
154        self.read_positions()
155    }
156}
157
158// ============================================================================
159// Convenience Functions (for backward compatibility)
160// ============================================================================
161
162/// Parse vertex positions from an OBJ file.
163/// Returns a vec of [x, y, z] positions.
164pub fn read_obj_positions<P: AsRef<Path>>(path: P) -> io::Result<Vec<[f32; 3]>> {
165    let file = fs::File::open(path)?;
166    let reader = BufReader::new(file);
167    read_obj_positions_from_reader(reader)
168}
169
170fn read_obj_positions_from_reader<R: BufRead>(reader: R) -> io::Result<Vec<[f32; 3]>> {
171    let mut positions = Vec::new();
172
173    for line in reader.lines() {
174        let line = line?;
175        let trimmed = strip_obj_comment(&line);
176
177        if trimmed.starts_with("vn ") || trimmed.starts_with("vt ") {
178            continue;
179        }
180
181        if !trimmed.starts_with('v') {
182            continue;
183        }
184
185        let mut parts = trimmed.split_whitespace();
186        if parts.next() != Some("v") {
187            continue;
188        }
189
190        let x = parts.next().and_then(|s| s.parse().ok());
191        let y = parts.next().and_then(|s| s.parse().ok());
192        let z = parts.next().and_then(|s| s.parse().ok());
193
194        if let (Some(x), Some(y), Some(z)) = (x, y, z) {
195            positions.push([x, y, z]);
196        }
197    }
198
199    Ok(positions)
200}
201
202#[derive(Debug)]
203struct ParsedObjMesh {
204    positions: Vec<[f32; 3]>,
205    texcoords: Option<Vec<[f32; 2]>>,
206    normals: Option<Vec<[f32; 3]>>,
207    faces: Vec<[u32; 3]>,
208}
209
210#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
211struct ObjVertexRef {
212    position: usize,
213    texcoord: Option<usize>,
214    normal: Option<usize>,
215}
216
217fn strip_obj_comment(line: &str) -> &str {
218    line.split('#').next().unwrap_or("").trim()
219}
220
221fn make_f32x3_attribute(
222    attribute_type: GeometryAttributeType,
223    values: &[[f32; 3]],
224) -> PointAttribute {
225    let mut attribute = PointAttribute::new();
226    attribute.init(attribute_type, 3, DataType::Float32, false, values.len());
227
228    let buffer = attribute.buffer_mut();
229    for (i, value) in values.iter().enumerate() {
230        let bytes: Vec<u8> = value
231            .iter()
232            .flat_map(|component| component.to_le_bytes())
233            .collect();
234        buffer.write(i * 12, &bytes);
235    }
236
237    attribute
238}
239
240fn make_f32x2_attribute(
241    attribute_type: GeometryAttributeType,
242    values: &[[f32; 2]],
243) -> PointAttribute {
244    let mut attribute = PointAttribute::new();
245    attribute.init(attribute_type, 2, DataType::Float32, false, values.len());
246
247    let buffer = attribute.buffer_mut();
248    for (i, value) in values.iter().enumerate() {
249        let bytes: Vec<u8> = value
250            .iter()
251            .flat_map(|component| component.to_le_bytes())
252            .collect();
253        buffer.write(i * 8, &bytes);
254    }
255
256    attribute
257}
258
259fn parse_obj_index(token: &str, len: usize, label: &str) -> io::Result<usize> {
260    let raw = token
261        .parse::<i32>()
262        .map_err(|_| invalid_obj(format!("Bad {label} index: {token}")))?;
263    if raw == 0 {
264        return Err(invalid_obj(format!("OBJ {label} indices are 1-based")));
265    }
266
267    let index = if raw > 0 { raw - 1 } else { len as i32 + raw };
268
269    if index < 0 || index as usize >= len {
270        return Err(invalid_obj(format!(
271            "OBJ {label} index {raw} is out of range for {len} values"
272        )));
273    }
274
275    Ok(index as usize)
276}
277
278fn parse_face_vertex(
279    token: &str,
280    position_count: usize,
281    texcoord_count: usize,
282    normal_count: usize,
283) -> io::Result<ObjVertexRef> {
284    let parts: Vec<&str> = token.split('/').collect();
285    if parts.is_empty() || parts.len() > 3 || parts[0].is_empty() {
286        return Err(invalid_obj(format!("Bad OBJ face vertex: {token}")));
287    }
288
289    let position = parse_obj_index(parts[0], position_count, "position")?;
290    let texcoord = if parts.get(1).is_some_and(|part| !part.is_empty()) {
291        Some(parse_obj_index(parts[1], texcoord_count, "texcoord")?)
292    } else {
293        None
294    };
295    let normal = if parts.get(2).is_some_and(|part| !part.is_empty()) {
296        Some(parse_obj_index(parts[2], normal_count, "normal")?)
297    } else {
298        None
299    };
300
301    Ok(ObjVertexRef {
302        position,
303        texcoord,
304        normal,
305    })
306}
307
308fn push_obj_vertex(
309    vertex_ref: ObjVertexRef,
310    vertex_map: &mut HashMap<ObjVertexRef, u32>,
311    vertices: &mut Vec<ObjVertexRef>,
312) -> u32 {
313    if let Some(&point_id) = vertex_map.get(&vertex_ref) {
314        return point_id;
315    }
316
317    let point_id = vertices.len() as u32;
318    vertices.push(vertex_ref);
319    vertex_map.insert(vertex_ref, point_id);
320    point_id
321}
322
323fn invalid_obj(message: impl Into<String>) -> io::Error {
324    io::Error::new(io::ErrorKind::InvalidData, message.into())
325}
326
327fn read_obj_mesh_from_reader<R: BufRead>(reader: R) -> io::Result<ParsedObjMesh> {
328    let mut source_positions = Vec::new();
329    let mut source_texcoords = Vec::new();
330    let mut source_normals = Vec::new();
331    let mut faces = Vec::new();
332    let mut vertices = Vec::new();
333    let mut vertex_map = HashMap::new();
334
335    for line in reader.lines() {
336        let line = line?;
337        let trimmed = strip_obj_comment(&line);
338
339        if trimmed.starts_with("v ") {
340            let mut parts = trimmed.split_whitespace();
341            parts.next(); // skip 'v'
342
343            let x = parts.next().and_then(|s| s.parse().ok());
344            let y = parts.next().and_then(|s| s.parse().ok());
345            let z = parts.next().and_then(|s| s.parse().ok());
346
347            if let (Some(x), Some(y), Some(z)) = (x, y, z) {
348                source_positions.push([x, y, z]);
349            }
350        } else if trimmed.starts_with("vt ") {
351            let mut parts = trimmed.split_whitespace();
352            parts.next(); // skip 'vt'
353
354            let u = parts.next().and_then(|s| s.parse().ok());
355            let v = parts.next().and_then(|s| s.parse().ok());
356
357            if let (Some(u), Some(v)) = (u, v) {
358                source_texcoords.push([u, v]);
359            }
360        } else if trimmed.starts_with("vn ") {
361            let mut parts = trimmed.split_whitespace();
362            parts.next(); // skip 'vn'
363
364            let x = parts.next().and_then(|s| s.parse().ok());
365            let y = parts.next().and_then(|s| s.parse().ok());
366            let z = parts.next().and_then(|s| s.parse().ok());
367
368            if let (Some(x), Some(y), Some(z)) = (x, y, z) {
369                source_normals.push([x, y, z]);
370            }
371        } else if trimmed.starts_with("f ") {
372            let mut parts = trimmed.split_whitespace();
373            parts.next(); // skip 'f'
374
375            let face_vertices = parts
376                .map(|part| {
377                    parse_face_vertex(
378                        part,
379                        source_positions.len(),
380                        source_texcoords.len(),
381                        source_normals.len(),
382                    )
383                })
384                .collect::<io::Result<Vec<_>>>()?;
385            if face_vertices.len() < 3 {
386                continue;
387            }
388
389            for i in 1..face_vertices.len() - 1 {
390                let triangle = [face_vertices[0], face_vertices[i], face_vertices[i + 1]];
391                faces.push([
392                    push_obj_vertex(triangle[0], &mut vertex_map, &mut vertices),
393                    push_obj_vertex(triangle[1], &mut vertex_map, &mut vertices),
394                    push_obj_vertex(triangle[2], &mut vertex_map, &mut vertices),
395                ]);
396            }
397        }
398    }
399
400    if faces.is_empty() {
401        return Ok(ParsedObjMesh {
402            positions: source_positions,
403            texcoords: None,
404            normals: None,
405            faces,
406        });
407    }
408
409    let uses_texcoords = vertices.iter().any(|vertex| vertex.texcoord.is_some());
410    let uses_normals = vertices.iter().any(|vertex| vertex.normal.is_some());
411    if uses_texcoords && vertices.iter().any(|vertex| vertex.texcoord.is_none()) {
412        return Err(invalid_obj(
413            "OBJ texture coordinate indices must be present on every face vertex",
414        ));
415    }
416    if uses_normals && vertices.iter().any(|vertex| vertex.normal.is_none()) {
417        return Err(invalid_obj(
418            "OBJ normal indices must be present on every face vertex",
419        ));
420    }
421
422    let positions = vertices
423        .iter()
424        .map(|vertex| source_positions[vertex.position])
425        .collect();
426    let texcoords = uses_texcoords.then(|| {
427        vertices
428            .iter()
429            .map(|vertex| source_texcoords[vertex.texcoord.unwrap()])
430            .collect()
431    });
432    let normals = uses_normals.then(|| {
433        vertices
434            .iter()
435            .map(|vertex| source_normals[vertex.normal.unwrap()])
436            .collect()
437    });
438
439    Ok(ParsedObjMesh {
440        positions,
441        texcoords,
442        normals,
443        faces,
444    })
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use std::io::Write;
451    use tempfile::NamedTempFile;
452
453    #[test]
454    fn test_read_obj_positions() {
455        let mut file = NamedTempFile::new().unwrap();
456        writeln!(file, "# comment").unwrap();
457        writeln!(file, "v 1.0 2.0 3.0").unwrap();
458        writeln!(file, "v 4.5 5.5 6.5").unwrap();
459        writeln!(file, "vn 0 1 0").unwrap();
460        writeln!(file, "vt 0.5 0.5").unwrap();
461        writeln!(file, "v -1.0 -2.0 -3.0").unwrap();
462        file.flush().unwrap();
463
464        let positions = read_obj_positions(file.path()).unwrap();
465        assert_eq!(positions.len(), 3);
466        assert_eq!(positions[0], [1.0, 2.0, 3.0]);
467        assert_eq!(positions[1], [4.5, 5.5, 6.5]);
468        assert_eq!(positions[2], [-1.0, -2.0, -3.0]);
469    }
470
471    #[test]
472    fn test_read_obj_mesh_preserves_normals_and_texcoords() {
473        let obj = br#"
474v 0.0 0.0 0.0
475v 1.0 0.0 0.0
476v 1.0 1.0 0.0
477v 0.0 1.0 0.0
478vt 0.0 0.0
479vt 1.0 0.0
480vt 1.0 1.0
481vt 0.0 1.0
482vn 0.0 0.0 1.0
483f 1/1/1 2/2/1 3/3/1 4/4/1
484"#;
485
486        let mut reader = ObjReader::from_bytes(obj.as_slice());
487        let mesh = reader.read_mesh().unwrap();
488
489        assert_eq!(mesh.num_points(), 4);
490        assert_eq!(mesh.num_faces(), 2);
491
492        let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
493        assert_eq!(normal_att.num_components(), 3);
494        assert_eq!(normal_att.data_type(), DataType::Float32);
495
496        let texcoord_att = mesh
497            .named_attribute(GeometryAttributeType::TexCoord)
498            .unwrap();
499        assert_eq!(texcoord_att.num_components(), 2);
500        assert_eq!(texcoord_att.data_type(), DataType::Float32);
501        assert_eq!(texcoord_att.buffer().data().len(), 4 * 2 * 4);
502    }
503}