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