tobj/lib.rs
1//! # Tiny OBJ Loader
2//!
3//! A tiny OBJ loader, inspired by Syoyo's excellent [`tinyobjloader`](https://github.com/syoyo/tinyobjloader).
4//! Aims to be a simple and lightweight option for loading `OBJ` files.
5//!
6//! Just returns two `Vec`s containing loaded models and materials.
7//!
8//! ## Triangulation
9//!
10//! Meshes can be triangulated on the fly or left as-is.
11//!
12//! Only polygons that are trivially convertible to triangle fans are supported.
13//! Arbitrary polygons may not behave as expected. The best solution would be to
14//! convert your mesh to solely consist of triangles in your modeling software.
15//!
16//! ## Optional – Normals & Texture Coordinates
17//!
18//! It is assumed that all meshes will at least have positions, but normals and
19//! texture coordinates are optional.
20//!
21//! If no normals or texture coordinates are found then the corresponding
22//! [`Vec`](Mesh::normals)s for the [`Mesh`] will be empty.
23//!
24//! ## Flat Data
25//!
26//! Values are stored packed as [`f32`]s (or [`f64`]s with the use_f64 feature)
27//! in flat `Vec`s.
28//!
29//! For example, the `positions` member of a `Mesh` will contain `[x, y, z, x,
30//! y, z, ...]` which you can then use however you like.
31//!
32//! ## Indices
33//!
34//! Indices are also loaded and may re-use vertices already existing in the
35//! mesh, this data is stored in the [`indices`](Mesh::indices) member.
36//!
37//! When a `Mesh` contains *per vertex per face* normals or texture coordinates,
38//! positions can be duplicated to be *per vertex per face* too via the
39//! [`single_index`](LoadOptions::single_index) flag. This potentially changes
40//! the topology (faces may become disconnected even though their vertices still
41//! share a position in space).
42//!
43//! By default separate indices for normals and texture coordinates are created.
44//! This also guarantees that the topology of the `Mesh` does *not* change when
45//! either of the latter are specified *per vertex per face*.
46//!
47//! ## Materials
48//!
49//! Standard `MTL` attributes are supported too. Any unrecognized parameters
50//! will be stored in a `HashMap` containing the key-value pairs of the
51//! unrecognized parameter and its value.
52//!
53//! ## Example
54//!
55//! In this simple example we load the classic Cornell Box model that only
56//! defines positions and print out its attributes. This example is a slightly
57//! trimmed down version of `print_model_info` and `print_material_info`
58//! combined together, see them for a version that also prints out normals and
59//! texture coordinates if the model has them.
60//!
61//! The [`LoadOptions`] used are typical for the case when the mesh is going to
62//! be sent to a realtime rendering context (game engine, GPU etc.).
63//!
64//! ```
65//! use tobj;
66//!
67//! let cornell_box = tobj::load_obj("obj/cornell_box.obj", &tobj::GPU_LOAD_OPTIONS);
68//! assert!(cornell_box.is_ok());
69//!
70//! let (models, materials) = cornell_box.expect("Failed to load OBJ file");
71//!
72//! // Materials might report a separate loading error if the MTL file wasn't found.
73//! // If you don't need the materials, you can generate a default here and use that
74//! // instead.
75//! let materials = materials.expect("Failed to load MTL file");
76//!
77//! println!("# of models: {}", models.len());
78//! println!("# of materials: {}", materials.len());
79//!
80//! for (i, m) in models.iter().enumerate() {
81//! let mesh = &m.mesh;
82//!
83//! println!("model[{}].name = \'{}\'", i, m.name);
84//! println!("model[{}].mesh.material_id = {:?}", i, mesh.material_id);
85//!
86//! println!(
87//! "Size of model[{}].face_arities: {}",
88//! i,
89//! mesh.face_arities.len()
90//! );
91//!
92//! let mut next_face = 0;
93//! for f in 0..mesh.face_arities.len() {
94//! let end = next_face + mesh.face_arities[f] as usize;
95//! let face_indices: Vec<_> = mesh.indices[next_face..end].iter().collect();
96//! println!(" face[{}] = {:?}", f, face_indices);
97//! next_face = end;
98//! }
99//!
100//! // Normals and texture coordinates are also loaded, but not printed in this example
101//! println!("model[{}].vertices: {}", i, mesh.positions.len() / 3);
102//!
103//! assert!(mesh.positions.len() % 3 == 0);
104//! for v in 0..mesh.positions.len() / 3 {
105//! println!(
106//! " v[{}] = ({}, {}, {})",
107//! v,
108//! mesh.positions[3 * v],
109//! mesh.positions[3 * v + 1],
110//! mesh.positions[3 * v + 2]
111//! );
112//! }
113//! }
114//!
115//! for (i, m) in materials.iter().enumerate() {
116//! println!("material[{}].name = \'{}\'", i, m.name);
117//! if let Some(ambient) = m.ambient {
118//! println!(
119//! " material.Ka = ({}, {}, {})",
120//! ambient[0], ambient[1], ambient[2]
121//! );
122//! }
123//! if let Some(diffuse) = m.diffuse {
124//! println!(
125//! " material.Kd = ({}, {}, {})",
126//! diffuse[0], diffuse[1], diffuse[2]
127//! );
128//! }
129//! if let Some(specular) = m.specular {
130//! println!(
131//! " material.Ks = ({}, {}, {})",
132//! specular[0], specular[1], specular[2]
133//! );
134//! }
135//! if let Some(emissive) = m.emissive {
136//! println!(
137//! " material.Ke = ({}, {}, {})",
138//! emissive[0], emissive[1], emissive[2]
139//! );
140//! }
141//! if let Some(shininess) = m.shininess {
142//! println!(" material.Ns = {}", shininess);
143//! }
144//! if let Some(dissolve) = m.dissolve {
145//! println!(" material.d = {}", dissolve);
146//! }
147//! if let Some(ambient_texture) = &m.ambient_texture {
148//! println!(" material.map_Ka = {}", ambient_texture);
149//! }
150//! if let Some(diffuse_texture) = &m.diffuse_texture {
151//! println!(" material.map_Kd = {}", diffuse_texture);
152//! }
153//! if let Some(specular_texture) = &m.specular_texture {
154//! println!(" material.map_Ks = {}", specular_texture);
155//! }
156//! if let Some(shininess_texture) = &m.shininess_texture {
157//! println!(" material.map_Ns = {}", shininess_texture);
158//! }
159//! if let Some(normal_texture) = &m.normal_texture {
160//! println!(" material.map_Bump = {}", normal_texture);
161//! }
162//! if let Some(dissolve_texture) = &m.dissolve_texture {
163//! println!(" material.map_d = {}", dissolve_texture);
164//! }
165//!
166//! for (k, v) in &m.unknown_param {
167//! println!(" material.{} = {}", k, v);
168//! }
169//! }
170//! ```
171//!
172//! ## Rendering Examples
173//!
174//! For an example of integration with [glium](https://github.com/tomaka/glium)
175//! to make a simple OBJ viewer, check out [`tobj viewer`](https://github.com/Twinklebear/tobj_viewer).
176//! Some more sample images can be found in [this gallery](http://imgur.com/a/xsg6v).
177//!
178//! The Rungholt model shown below is reasonably large (6.7M triangles, 12.3M
179//! vertices) and is loaded in ~7.47s using a peak of ~1.1GB of memory on a
180//! Windows 10 machine with an i7-4790k and 16GB of 1600Mhz DDR3 RAM with tobj
181//! 0.1.1 on rustc 1.6.0. The model can be found on [Morgan McGuire's](http://graphics.cs.williams.edu/data/meshes.xml)
182//! meshes page and was originally built by kescha. Future work will focus on
183//! improving performance and memory usage.
184//!
185//! <img src="http://i.imgur.com/wImyNG4.png" alt="Rungholt"
186//! style="display:block; max-width:100%; height:auto">
187//!
188//! For an example of integration within a ray tracer, check out tray\_rust's
189//! [mesh module](https://github.com/Twinklebear/tray_rust/blob/master/src/geometry/mesh.rs).
190//! The Stanford Buddha and Dragon from the
191//! [Stanford 3D Scanning Repository](http://graphics.stanford.edu/data/3Dscanrep/)
192//! both load quite quickly. The Rust logo model was made by [Nylithius on BlenderArtists](http://blenderartists.org/forum/showthread.php?362836-Rust-language-3D-logo).
193//! The materials used are from the [MERL BRDF Database](http://www.merl.com/brdf/).
194//!
195//! <img src="http://i.imgur.com/E1ylrZW.png" alt="Rust logo with friends"
196//! style="display:block; max-width:100%; height:auto">
197//!
198//! ## Features
199//!
200//! * [`ahash`](https://crates.io/crates/ahash) – On by default. Use [`AHashMap`](https://docs.rs/ahash/latest/ahash/struct.AHashMap.html)
201//! for hashing when reading files and merging vertices. To disable and use
202//! the slower [`HashMap`](std::collections::HashMap) instead, unset default
203//! features in `Cargo.toml`:
204//!
205//! ```toml
206//! [dependencies.tobj]
207//! default-features = false
208//! ```
209//!
210//! * [`merging`](LoadOptions::merge_identical_points) – Adds support for
211//! merging identical vertex positions on disconnected faces during import.
212//!
213//! **Warning:** this feature uses *const generics* and thus requires at
214//! least a `beta` toolchain to build.
215//!
216//! * [`reordering`](LoadOptions::reorder_data) – Adds support for reordering
217//! the normal- and texture coordinate indices.
218//!
219//! * [`async`](load_obj_buf_async) – Adds support for async loading of obj
220//! files from a buffer, with an async material loader. Useful in environments
221//! that do not support blocking IO (e.g. WebAssembly).
222//!
223//! * [`futures`](futures) - Adds support for async loading of objs and materials
224//! using [futures](https://crates.io/crates/futures) [AsyncRead](futures_lite::AsyncRead)
225//! traits.
226//!
227//! * [`tokio`](tokio) - Adds support for async loading of objs and materials
228//! using [tokio](https://crates.io/crates/tokio) [AsyncRead](::tokio::io::AsyncRead)
229//! traits.
230//!
231//! * ['use_f64'] - Uses double-precision (f64) instead of single-precision
232//! (f32) floating point types
233#![cfg_attr(feature = "merging", allow(incomplete_features))]
234#![cfg_attr(feature = "merging", feature(generic_const_exprs))]
235
236#[cfg(test)]
237mod tests;
238
239use std::{
240 error::Error,
241 fmt,
242 fs::File,
243 io::{prelude::*, BufReader},
244 path::{Path, PathBuf},
245 str::{FromStr, SplitWhitespace},
246};
247
248#[cfg(feature = "use_f64")]
249type Float = f64;
250
251#[cfg(not(feature = "use_f64"))]
252type Float = f32;
253
254#[cfg(feature = "async")]
255use std::future::Future;
256
257#[cfg(feature = "merging")]
258use std::mem::size_of;
259
260#[cfg(feature = "ahash")]
261type HashMap<K, V> = ahash::AHashMap<K, V>;
262
263#[cfg(not(feature = "ahash"))]
264type HashMap<K, V> = std::collections::HashMap<K, V>;
265
266/// Typical [`LoadOptions`] for using meshes in a GPU/relatime context.
267///
268/// Faces are *triangulated*, a *single index* is generated and *degenerate
269/// faces* (points & lines) are *discarded*.
270pub const GPU_LOAD_OPTIONS: LoadOptions = LoadOptions {
271 #[cfg(feature = "merging")]
272 merge_identical_points: false,
273 #[cfg(feature = "reordering")]
274 reorder_data: false,
275 single_index: true,
276 triangulate: true,
277 ignore_points: true,
278 ignore_lines: true,
279};
280
281/// Typical [`LoadOptions`] for using meshes with an offline rendeder.
282///
283/// Faces are *kept as they are* (e.g. n-gons) and *normal and texture
284/// coordinate data is reordered* so only a single index is needed.
285/// Topology remains unchanged except for *degenerate faces* (points & lines)
286/// which are *discarded*.
287pub const OFFLINE_RENDERING_LOAD_OPTIONS: LoadOptions = LoadOptions {
288 #[cfg(feature = "merging")]
289 merge_identical_points: true,
290 #[cfg(feature = "reordering")]
291 reorder_data: true,
292 single_index: false,
293 triangulate: false,
294 ignore_points: true,
295 ignore_lines: true,
296};
297
298/// A mesh made up of triangles loaded from some `OBJ` file.
299///
300/// It is assumed that all meshes will at least have positions, but normals and
301/// texture coordinates are optional. If no normals or texture coordinates where
302/// found then the corresponding `Vec`s in the `Mesh` will be empty. Values are
303/// stored packed as [`f32`]s (or [`f64`]s with the use_f64 feature) in flat
304/// `Vec`s.
305///
306/// For examples the `positions` member of a loaded mesh will contain `[x, y, z,
307/// x, y, z, ...]` which you can then use however you like. Indices are also
308/// loaded and may re-use vertices already existing in the mesh. This data is
309/// stored in the `indices` member.
310///
311/// # Example:
312/// Load the Cornell box and get the attributes of the first vertex. It's
313/// assumed all meshes will have positions (required), but normals and texture
314/// coordinates are optional, in which case the corresponding `Vec` will be
315/// empty.
316///
317/// ```
318/// let cornell_box = tobj::load_obj("obj/cornell_box.obj", &tobj::GPU_LOAD_OPTIONS);
319/// assert!(cornell_box.is_ok());
320///
321/// let (models, materials) = cornell_box.unwrap();
322///
323/// let mesh = &models[0].mesh;
324/// let i = mesh.indices[0] as usize;
325///
326/// // pos = [x, y, z]
327/// let pos = [
328/// mesh.positions[i * 3],
329/// mesh.positions[i * 3 + 1],
330/// mesh.positions[i * 3 + 2],
331/// ];
332///
333/// if !mesh.normals.is_empty() {
334/// // normal = [x, y, z]
335/// let normal = [
336/// mesh.normals[i * 3],
337/// mesh.normals[i * 3 + 1],
338/// mesh.normals[i * 3 + 2],
339/// ];
340/// }
341///
342/// if !mesh.texcoords.is_empty() {
343/// // texcoord = [u, v];
344/// let texcoord = [mesh.texcoords[i * 2], mesh.texcoords[i * 2 + 1]];
345/// }
346/// ```
347#[derive(Debug, Clone, Default)]
348pub struct Mesh {
349 /// Flattened 3 component floating point vectors, storing positions of
350 /// vertices in the mesh.
351 pub positions: Vec<Float>,
352 /// Flattened 3 component floating point vectors, storing the color
353 /// associated with the vertices in the mesh.
354 ///
355 /// Most meshes do not have vertex colors. If no vertex colors are specified
356 /// this will be empty.
357 pub vertex_color: Vec<Float>,
358 /// Flattened 3 component floating point vectors, storing normals of
359 /// vertices in the mesh.
360 ///
361 /// Not all meshes have normals. If no normals are specified this will
362 /// be empty.
363 pub normals: Vec<Float>,
364 /// Flattened 2 component floating point vectors, storing texture
365 /// coordinates of vertices in the mesh.
366 ///
367 /// Not all meshes have texture coordinates. If no texture coordinates are
368 /// specified this will be empty.
369 pub texcoords: Vec<Float>,
370 /// Indices for vertices of each face. If loaded with
371 /// [`triangulate`](LoadOptions::triangulate) set to `true` each face in the
372 /// mesh is a triangle.
373 ///
374 /// Otherwise [`face_arities`](Mesh::face_arities) indicates how many
375 /// indices are used by each face.
376 ///
377 /// When [`single_index`](LoadOptions::single_index) is set to `true`,
378 /// these indices are for *all* of the data in the mesh. Positions,
379 /// normals and texture coordinaes.
380 /// Otherwise normals and texture coordinates have *their own* indices,
381 /// each.
382 pub indices: Vec<u32>,
383 /// The number of vertices (arity) of each face. *Empty* if loaded with
384 /// `triangulate` set to `true` or if the mesh constists *only* of
385 /// triangles.
386 ///
387 /// The offset for the starting index of a face can be found by iterating
388 /// through the `face_arities` until reaching the desired face, accumulating
389 /// the number of vertices used so far.
390 pub face_arities: Vec<u32>,
391 /// The indices for vertex colors. Only present when the
392 /// [`merging`](LoadOptions::merge_identical_points) feature is enabled, and
393 /// empty unless the corresponding load option is set to `true`.
394 #[cfg(feature = "merging")]
395 pub vertex_color_indices: Vec<u32>,
396 /// The indices for texture coordinates. Can be omitted by setting
397 /// `single_index` to `true`.
398 pub texcoord_indices: Vec<u32>,
399 /// The indices for normals. Can be omitted by setting `single_index` to
400 /// `true`.
401 pub normal_indices: Vec<u32>,
402 /// Optional material id associated with this mesh. The material id indexes
403 /// into the Vec of Materials loaded from the associated `MTL` file
404 pub material_id: Option<usize>,
405}
406
407/// Options for processing the mesh during loading.
408///
409/// Passed to [`load_obj()`], [`load_obj_buf()`] and [`load_obj_buf_async()`].
410///
411/// By default, all of these are `false`. With those settings, the data you get
412/// represents the original data in the input file/buffer as closely as
413/// possible.
414///
415/// Use the [init struct pattern](https://xaeroxe.github.io/init-struct-pattern/) to set individual options:
416/// ```ignore
417/// LoadOptions {
418/// single_index: true,
419/// ..Default::default()
420/// }
421/// ```
422///
423/// There are convenience `const`s for the most common cases:
424///
425/// * [`GPU_LOAD_OPTIONS`] – if you display meshes on the GPU/in realtime.
426///
427/// * [`OFFLINE_RENDERING_LOAD_OPTIONS`] – if you're rendering meshes with e.g.
428/// an offline path tracer or the like.
429#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
430#[derive(Debug, Default, Clone, Copy)]
431pub struct LoadOptions {
432 /// Merge identical positions.
433 ///
434 /// This is usually what you want if you intend to use the mesh in an
435 /// *offline rendering* context or to do further processing with
436 /// *topological operators*.
437 ///
438 /// * This flag is *mutually exclusive* with
439 /// [`single_index`](LoadOptions::single_index) and will lead to a
440 /// [`InvalidLoadOptionConfig`](LoadError::InvalidLoadOptionConfig) error
441 /// if both are set to `true`.
442 ///
443 /// * If adjacent faces share vertices that have separate `indices` but the
444 /// same position in 3D they will be merged into a single vertex and the
445 /// resp. `indices` changed.
446 ///
447 /// * Topolgy may change as a result (faces may become *connected* in the
448 /// index).
449 #[cfg(feature = "merging")]
450 pub merge_identical_points: bool,
451 /// Normal & texture coordinates will be reordered to allow omitting their
452 /// indices.
453 ///
454 /// * This flag is *mutually exclusive* with
455 /// [`single_index`](LoadOptions::single_index) and will lead to an
456 /// [`InvalidLoadOptionConfig`](LoadError::InvalidLoadOptionConfig) error
457 /// if both are set to `true`.
458 ///
459 /// * The resulting [`Mesh`]'s `normal_indices` and/or `texcoord_indices`
460 /// will be empty.
461 ///
462 /// * *Per-vertex* normals and/or texture_coordinates will be reordered to
463 /// match the `Mesh`'s `indices`.
464 ///
465 /// * *Per-vertex-per-face* normals and/or texture coordinates indices will
466 /// be `[0, 1, 2, ..., n]`. I.e.:
467 ///
468 /// ```ignore
469 /// // If normals where specified per-vertex-per-face:
470 /// assert!(mesh.indices.len() == mesh.normals.len() / 3);
471 ///
472 /// for index in 0..mesh.indices.len() {
473 /// println!("Normal n is {}, {}, {}",
474 /// mesh.normals[index * 3 + 0],
475 /// mesh.normals[index * 3 + 1],
476 /// mesh.normals[index * 3 + 2]
477 /// );
478 /// }
479 /// ```
480 #[cfg(feature = "reordering")]
481 pub reorder_data: bool,
482 /// Create a single index.
483 ///
484 /// This is usually what you want if you are loading the mesh to display in
485 /// a *realtime* (*GPU*) context.
486 ///
487 /// * This flag is *mutually exclusive* with both
488 /// [`merge_identical_points`](LoadOptions::merge_identical_points) and
489 /// [`reorder_data`](LoadOptions::reorder_data) resp. and will lead to a
490 /// [`InvalidLoadOptionConfig`](LoadError::InvalidLoadOptionConfig) error
491 /// if both it and either of the two other are set to `true`.
492 ///
493 /// * Vertices may get duplicated to match the granularity
494 /// (*per-vertex-per-face*) of normals and/or texture coordinates.
495 ///
496 /// * Topolgy may change as a result (faces may become *disconnected* in the
497 /// index).
498 ///
499 /// * The resulting [`Mesh`]'s [`normal_indices`](Mesh::normal_indices) and
500 /// [`texcoord_indices`](Mesh::texcoord_indices) will be empty.
501 pub single_index: bool,
502 /// Triangulate all faces.
503 ///
504 /// * Points (one point) and lines (two points) are blown up to zero area
505 /// triangles via point duplication. Except if `ignore_points` or
506 /// `ignore_lines` is/are set to `true`, resp.
507 ///
508 /// * The resulting `Mesh`'s [`face_arities`](Mesh::face_arities) will be
509 /// empty as all faces are guranteed to have arity `3`.
510 ///
511 /// * Only polygons that are trivially convertible to triangle fans are
512 /// supported. Arbitrary polygons may not behave as expected. The best
513 /// solution would be to convert your mesh to solely consist of triangles
514 /// in your modeling software.
515 pub triangulate: bool,
516 /// Ignore faces containing only a single vertex (points).
517 ///
518 /// This is usually what you want if you do *not* intend to make special use
519 /// of the point data (e.g. as particles etc.).
520 ///
521 /// Polygon meshes that contain faces with one vertex only usually do so
522 /// because of bad topology.
523 pub ignore_points: bool,
524 /// Ignore faces containing only two vertices (lines).
525 ///
526 /// This is usually what you want if you do *not* intend to make special use
527 /// of the line data (e.g. as wires/ropes etc.).
528 ///
529 /// Polygon meshes that contains faces with two vertices only usually do so
530 /// because of bad topology.
531 pub ignore_lines: bool,
532}
533
534impl LoadOptions {
535 /// Checks if the given `LoadOptions` do not contain mutually exclusive flag
536 /// settings.
537 ///
538 /// This is called by [`load_obj()`]/[`load_obj_buf()`] in any case. This
539 /// method is only exposed for scenarios where you want to do this check
540 /// yourself.
541 pub fn is_valid(&self) -> bool {
542 // A = single_index, B = merge_identical_points, C = reorder_data
543 // (A ∧ ¬B) ∨ (A ∧ ¬C) -> A ∧ ¬(B ∨ C)
544 #[allow(unused_mut)]
545 let mut other_flags = false;
546
547 #[cfg(feature = "merging")]
548 {
549 other_flags = other_flags || self.merge_identical_points;
550 }
551 #[cfg(feature = "reordering")]
552 {
553 other_flags = other_flags || self.reorder_data;
554 }
555
556 (self.single_index != other_flags) || (!self.single_index && !other_flags)
557 }
558}
559
560/// A named model within the file.
561///
562/// Associates some mesh with a name that was specified with an `o` or `g`
563/// keyword in the `OBJ` file.
564#[derive(Clone, Debug)]
565pub struct Model {
566 /// [`Mesh`] used by the model containing its geometry.
567 pub mesh: Mesh,
568 /// Name assigned to this `Mesh`.
569 pub name: String,
570}
571
572impl Model {
573 /// Create a new model, associating a name with a [`Mesh`].
574 pub fn new(mesh: Mesh, name: String) -> Model {
575 Model { mesh, name }
576 }
577}
578
579/// A material that may be referenced by one or more [`Mesh`]es.
580///
581/// Standard `MTL` attributes are supported. Any unrecognized parameters will be
582/// stored as key-value pairs in the `unknown_param`
583/// [`HashMap`](std::collections::HashMap), which maps the unknown parameter to
584/// the value set for it.
585///
586/// No path is pre-pended to the texture file names specified in the `MTL` file.
587#[derive(Clone, Debug, Default)]
588pub struct Material {
589 /// Material name as specified in the `MTL` file.
590 pub name: String,
591 /// Ambient color of the material.
592 pub ambient: Option<[Float; 3]>,
593 /// Diffuse color of the material.
594 pub diffuse: Option<[Float; 3]>,
595 /// Specular color of the material.
596 pub specular: Option<[Float; 3]>,
597 /// Emissive color of the material.
598 pub emissive: Option<[Float; 3]>,
599 /// Material shininess attribute. Also called `glossiness`.
600 pub shininess: Option<Float>,
601 /// Dissolve attribute is the alpha term for the material. Referred to as
602 /// dissolve since that's what the `MTL` file format docs refer to it as.
603 pub dissolve: Option<Float>,
604 /// Optical density also known as index of refraction. Called
605 /// `optical_density` in the `MTL` specc. Takes on a value between 0.001
606 /// and 10.0. 1.0 means light does not bend as it passes through
607 /// the object.
608 pub optical_density: Option<Float>,
609 /// Name of the ambient texture file for the material.
610 pub ambient_texture: Option<String>,
611 /// Name of the diffuse texture file for the material.
612 pub diffuse_texture: Option<String>,
613 /// Name of the specular texture file for the material.
614 pub specular_texture: Option<String>,
615 /// Name of the normal map texture file for the material.
616 pub normal_texture: Option<String>,
617 /// Name of the shininess map texture file for the material.
618 pub shininess_texture: Option<String>,
619 /// Name of the alpha/opacity map texture file for the material.
620 ///
621 /// Referred to as `dissolve` to match the `MTL` file format specification.
622 pub dissolve_texture: Option<String>,
623 /// The illumnination model to use for this material. The different
624 /// illumination models are specified in the [`MTL` spec](http://paulbourke.net/dataformats/mtl/).
625 pub illumination_model: Option<u8>,
626 /// Key value pairs of any unrecognized parameters encountered while parsing
627 /// the material.
628 pub unknown_param: HashMap<String, String>,
629}
630
631/// Possible errors that may occur while loading `OBJ` and `MTL` files.
632#[derive(Debug, Clone, Copy, PartialEq)]
633pub enum LoadError {
634 OpenFileFailed,
635 ReadError,
636 UnrecognizedCharacter,
637 PositionParseError,
638 NormalParseError,
639 TexcoordParseError,
640 FaceParseError,
641 MaterialParseError,
642 InvalidObjectName,
643 InvalidPolygon,
644 FaceVertexOutOfBounds,
645 FaceTexCoordOutOfBounds,
646 FaceNormalOutOfBounds,
647 FaceColorOutOfBounds,
648 InvalidLoadOptionConfig,
649 GenericFailure,
650}
651
652impl fmt::Display for LoadError {
653 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
654 let msg = match *self {
655 LoadError::OpenFileFailed => "open file failed",
656 LoadError::ReadError => "read error",
657 LoadError::UnrecognizedCharacter => "unrecognized character",
658 LoadError::PositionParseError => "position parse error",
659 LoadError::NormalParseError => "normal parse error",
660 LoadError::TexcoordParseError => "texcoord parse error",
661 LoadError::FaceParseError => "face parse error",
662 LoadError::MaterialParseError => "material parse error",
663 LoadError::InvalidObjectName => "invalid object name",
664 LoadError::InvalidPolygon => "invalid polygon",
665 LoadError::FaceVertexOutOfBounds => "face vertex index out of bounds",
666 LoadError::FaceTexCoordOutOfBounds => "face texcoord index out of bounds",
667 LoadError::FaceNormalOutOfBounds => "face normal index out of bounds",
668 LoadError::FaceColorOutOfBounds => "face vertex color index out of bounds",
669 LoadError::InvalidLoadOptionConfig => "mutually exclusive load options",
670 LoadError::GenericFailure => "generic failure",
671 };
672
673 f.write_str(msg)
674 }
675}
676
677impl Error for LoadError {}
678
679/// A [`Result`] containing all the models loaded from the file and any
680/// materials from referenced material libraries. Or an error that occured while
681/// loading.
682pub type LoadResult = Result<(Vec<Model>, Result<Vec<Material>, LoadError>), LoadError>;
683
684/// A [`Result`] containing all the materials loaded from the file and a map of
685/// `MTL` name to index. Or an error that occured while loading.
686pub type MTLLoadResult = Result<(Vec<Material>, HashMap<String, usize>), LoadError>;
687
688/// Struct storing indices corresponding to the vertex.
689///
690/// Some vertices may not have texture coordinates or normals, 0 is used to
691/// indicate this as OBJ indices begin at 1
692#[derive(Hash, Eq, PartialEq, PartialOrd, Ord, Debug, Copy, Clone)]
693struct VertexIndices {
694 pub v: usize,
695 pub vt: usize,
696 pub vn: usize,
697}
698
699static MISSING_INDEX: usize = usize::MAX;
700
701impl VertexIndices {
702 /// Parse the vertex indices from the face string.
703 ///
704 /// Valid face strings are those that are valid for a Wavefront `OBJ` file.
705 ///
706 /// Also handles relative face indices (negative values) which is why
707 /// passing the number of positions, texcoords and normals is required.
708 ///
709 /// Returns `None` if the face string is invalid.
710 fn parse(
711 face_str: &str,
712 pos_sz: usize,
713 tex_sz: usize,
714 norm_sz: usize,
715 ) -> Option<VertexIndices> {
716 let mut indices = [MISSING_INDEX; 3];
717 for i in face_str.split('/').enumerate() {
718 // Catch case of v//vn where we'll find an empty string in one of our splits
719 // since there are no texcoords for the mesh.
720 if !i.1.is_empty() {
721 match isize::from_str(i.1) {
722 Ok(x) => {
723 // Handle relative indices
724 *indices.get_mut(i.0)? = if x < 0 {
725 match i.0 {
726 0 => (pos_sz as isize + x) as _,
727 1 => (tex_sz as isize + x) as _,
728 2 => (norm_sz as isize + x) as _,
729 _ => return None, // Invalid number of elements for a face
730 }
731 } else {
732 (x - 1) as _
733 };
734 }
735 Err(_) => return None,
736 }
737 }
738 }
739 Some(VertexIndices {
740 v: indices[0],
741 vt: indices[1],
742 vn: indices[2],
743 })
744 }
745}
746
747/// Enum representing a face, storing indices for the face vertices.
748#[derive(Debug)]
749enum Face {
750 Point(VertexIndices),
751 Line(VertexIndices, VertexIndices),
752 Triangle(VertexIndices, VertexIndices, VertexIndices),
753 Quad(VertexIndices, VertexIndices, VertexIndices, VertexIndices),
754 Polygon(Vec<VertexIndices>),
755}
756
757/// Parse the float information from the words. Words is an iterator over the
758/// float strings. Returns `false` if parsing failed.
759fn parse_floatn(val_str: &mut SplitWhitespace, vals: &mut Vec<Float>, n: usize) -> bool {
760 let sz = vals.len();
761 for p in val_str.take(n) {
762 match FromStr::from_str(p) {
763 Ok(x) => vals.push(x),
764 Err(_) => return false,
765 }
766 }
767 // Require that we found the desired number of floats.
768 sz + n == vals.len()
769}
770
771/// Parse the a string into a float3 array, returns an error if parsing failed
772fn parse_float3(val_str: SplitWhitespace) -> Result<[Float; 3], LoadError> {
773 let arr: [Float; 3] = val_str
774 .take(3)
775 .map(FromStr::from_str)
776 .collect::<Result<Vec<_>, _>>()
777 .map_err(|_| LoadError::MaterialParseError)?
778 .try_into()
779 .unwrap();
780 Ok(arr)
781}
782
783/// Parse the a string into a float value, returns an error if parsing failed
784fn parse_float(val_str: Option<&str>) -> Result<Float, LoadError> {
785 val_str
786 .map(FromStr::from_str)
787 .map_or(Err(LoadError::MaterialParseError), |v| {
788 v.map_err(|_| LoadError::MaterialParseError)
789 })
790}
791
792/// Parse vertex indices for a face and append it to the list of faces passed.
793///
794/// Also handles relative face indices (negative values) which is why passing
795/// the number of positions, texcoords and normals is required.
796///
797/// Returns `false` if an error occured parsing the face.
798fn parse_face(
799 face_str: SplitWhitespace,
800 faces: &mut Vec<Face>,
801 pos_sz: usize,
802 tex_sz: usize,
803 norm_sz: usize,
804) -> bool {
805 let mut indices = Vec::new();
806 for f in face_str {
807 match VertexIndices::parse(f, pos_sz, tex_sz, norm_sz) {
808 Some(v) => indices.push(v),
809 None => return false,
810 }
811 }
812 // Check what kind face we read and push it on
813 match indices.len() {
814 1 => faces.push(Face::Point(indices[0])),
815 2 => faces.push(Face::Line(indices[0], indices[1])),
816 3 => faces.push(Face::Triangle(indices[0], indices[1], indices[2])),
817 4 => faces.push(Face::Quad(indices[0], indices[1], indices[2], indices[3])),
818 _ => faces.push(Face::Polygon(indices)),
819 }
820 true
821}
822
823/// Add a vertex to a mesh by either re-using an existing index (e.g. it's in
824/// the `index_map`) or appending the position, texcoord and normal as
825/// appropriate and creating a new vertex.
826fn add_vertex(
827 mesh: &mut Mesh,
828 index_map: &mut HashMap<VertexIndices, u32>,
829 vert: &VertexIndices,
830 pos: &[Float],
831 v_color: &[Float],
832 texcoord: &[Float],
833 normal: &[Float],
834) -> Result<(), LoadError> {
835 match index_map.get(vert) {
836 Some(&i) => mesh.indices.push(i),
837 None => {
838 let v = vert.v;
839 if v.saturating_mul(3).saturating_add(2) >= pos.len() {
840 return Err(LoadError::FaceVertexOutOfBounds);
841 }
842 // Add the vertex to the mesh
843 mesh.positions.push(pos[v * 3]);
844 mesh.positions.push(pos[v * 3 + 1]);
845 mesh.positions.push(pos[v * 3 + 2]);
846 if !texcoord.is_empty() && vert.vt != MISSING_INDEX {
847 let vt = vert.vt;
848 if vt * 2 + 1 >= texcoord.len() {
849 return Err(LoadError::FaceTexCoordOutOfBounds);
850 }
851 mesh.texcoords.push(texcoord[vt * 2]);
852 mesh.texcoords.push(texcoord[vt * 2 + 1]);
853 }
854 if !normal.is_empty() && vert.vn != MISSING_INDEX {
855 let vn = vert.vn;
856 if vn * 3 + 2 >= normal.len() {
857 return Err(LoadError::FaceNormalOutOfBounds);
858 }
859 mesh.normals.push(normal[vn * 3]);
860 mesh.normals.push(normal[vn * 3 + 1]);
861 mesh.normals.push(normal[vn * 3 + 2]);
862 }
863 if !v_color.is_empty() {
864 if v * 3 + 2 >= v_color.len() {
865 return Err(LoadError::FaceColorOutOfBounds);
866 }
867 mesh.vertex_color.push(v_color[v * 3]);
868 mesh.vertex_color.push(v_color[v * 3 + 1]);
869 mesh.vertex_color.push(v_color[v * 3 + 2]);
870 }
871 let next = index_map.len() as u32;
872 mesh.indices.push(next);
873 index_map.insert(*vert, next);
874 }
875 }
876 Ok(())
877}
878
879/// Export a list of faces to a mesh and return it, optionally converting quads
880/// to tris.
881fn export_faces(
882 pos: &[Float],
883 v_color: &[Float],
884 texcoord: &[Float],
885 normal: &[Float],
886 faces: &[Face],
887 mat_id: Option<usize>,
888 load_options: &LoadOptions,
889) -> Result<Mesh, LoadError> {
890 let mut index_map = HashMap::new();
891 let mut mesh = Mesh {
892 material_id: mat_id,
893 ..Default::default()
894 };
895 let mut is_all_triangles = true;
896
897 for f in faces {
898 // Optimized paths for Triangles and Quads, Polygon handles the general case of
899 // an unknown length triangle fan.
900 match *f {
901 Face::Point(ref a) => {
902 if !load_options.ignore_points {
903 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
904 if load_options.triangulate {
905 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
906 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
907 } else {
908 is_all_triangles = false;
909 mesh.face_arities.push(1);
910 }
911 }
912 }
913 Face::Line(ref a, ref b) => {
914 if !load_options.ignore_lines {
915 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
916 add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
917 if load_options.triangulate {
918 add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
919 } else {
920 is_all_triangles = false;
921 mesh.face_arities.push(2);
922 }
923 }
924 }
925 Face::Triangle(ref a, ref b, ref c) => {
926 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
927 add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
928 add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
929 if !load_options.triangulate {
930 mesh.face_arities.push(3);
931 }
932 }
933 Face::Quad(ref a, ref b, ref c, ref d) => {
934 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
935 add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
936 add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
937
938 if load_options.triangulate {
939 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
940 add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
941 add_vertex(&mut mesh, &mut index_map, d, pos, v_color, texcoord, normal)?;
942 } else {
943 add_vertex(&mut mesh, &mut index_map, d, pos, v_color, texcoord, normal)?;
944 is_all_triangles = false;
945 mesh.face_arities.push(4);
946 }
947 }
948 Face::Polygon(ref indices) => {
949 if load_options.triangulate {
950 let a = indices.first().ok_or(LoadError::InvalidPolygon)?;
951 let mut b = indices.get(1).ok_or(LoadError::InvalidPolygon)?;
952 for c in indices.iter().skip(2) {
953 add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
954 add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
955 add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
956 b = c;
957 }
958 } else {
959 for i in indices.iter() {
960 add_vertex(&mut mesh, &mut index_map, i, pos, v_color, texcoord, normal)?;
961 }
962 is_all_triangles = false;
963 mesh.face_arities.push(indices.len() as u32);
964 }
965 }
966 }
967 }
968
969 if is_all_triangles {
970 // This is a triangle-only mesh.
971 mesh.face_arities = Vec::new();
972 }
973
974 Ok(mesh)
975}
976
977/// Add a vertex to a mesh by either re-using an existing index (e.g. it's in
978/// the `index_map`) or appending the position, texcoord and normal as
979/// appropriate and creating a new vertex.
980#[allow(clippy::too_many_arguments)]
981#[inline]
982fn add_vertex_multi_index(
983 mesh: &mut Mesh,
984 index_map: &mut HashMap<usize, u32>,
985 normal_index_map: &mut HashMap<usize, u32>,
986 texcoord_index_map: &mut HashMap<usize, u32>,
987 vert: &VertexIndices,
988 pos: &[Float],
989 v_color: &[Float],
990 texcoord: &[Float],
991 normal: &[Float],
992) -> Result<(), LoadError> {
993 match index_map.get(&vert.v) {
994 Some(&i) => mesh.indices.push(i),
995 None => {
996 let vertex = vert.v;
997
998 if vertex.saturating_mul(3).saturating_add(2) >= pos.len() {
999 return Err(LoadError::FaceVertexOutOfBounds);
1000 }
1001
1002 // Add the vertex to the mesh.
1003 mesh.positions.push(pos[vertex * 3]);
1004 mesh.positions.push(pos[vertex * 3 + 1]);
1005 mesh.positions.push(pos[vertex * 3 + 2]);
1006
1007 let next = index_map.len() as u32;
1008 mesh.indices.push(next);
1009 index_map.insert(vertex, next);
1010
1011 // Also add vertex colors to the mesh if present.
1012 if !v_color.is_empty() {
1013 let vertex = vert.v;
1014
1015 if vertex * 3 + 2 >= v_color.len() {
1016 return Err(LoadError::FaceColorOutOfBounds);
1017 }
1018
1019 mesh.vertex_color.push(v_color[vertex * 3]);
1020 mesh.vertex_color.push(v_color[vertex * 3 + 1]);
1021 mesh.vertex_color.push(v_color[vertex * 3 + 2]);
1022 }
1023 }
1024 }
1025
1026 if !texcoord.is_empty() {
1027 let texcoord_indices = &mut mesh.texcoord_indices;
1028
1029 if MISSING_INDEX == vert.vt {
1030 // Special case: the very first vertex of the mesh has no index.
1031 if texcoord_indices.is_empty() {
1032 // We have no choice, simply reference the first vertex.
1033 mesh.texcoords.push(texcoord[0]);
1034 mesh.texcoords.push(texcoord[1]);
1035
1036 texcoord_indices.push(0);
1037 texcoord_index_map.insert(0, 0);
1038 // We use the previous index. Not great a fallback but less prone to
1039 // cause issues. FIXME: we should probably check if the
1040 // data is per-vertex-per-face and if so calculate the
1041 // average from adjacent face vertices.
1042 } else {
1043 texcoord_indices.push(*texcoord_indices.last().unwrap());
1044 }
1045 } else {
1046 match texcoord_index_map.get(&vert.vt) {
1047 Some(&index) => mesh.texcoord_indices.push(index as _),
1048 None => {
1049 let vt = vert.vt;
1050
1051 if vt * 2 + 1 >= texcoord.len() {
1052 return Err(LoadError::FaceTexCoordOutOfBounds);
1053 }
1054
1055 mesh.texcoords.push(texcoord[vt * 2]);
1056 mesh.texcoords.push(texcoord[vt * 2 + 1]);
1057
1058 let next = texcoord_index_map.len() as u32;
1059 mesh.texcoord_indices.push(next);
1060 texcoord_index_map.insert(vt, next);
1061 }
1062 }
1063 }
1064 }
1065
1066 if !normal.is_empty() {
1067 let normal_indices = &mut mesh.normal_indices;
1068 // The index is sparse – we need to make up a value.
1069 if MISSING_INDEX == vert.vn {
1070 // Special case: the very first vertex of the mesh has no index.
1071 if normal_indices.is_empty() {
1072 // We have no choice, simply reference the first vertex.
1073 mesh.normals.push(normal[0]);
1074 mesh.normals.push(normal[1]);
1075 mesh.normals.push(normal[2]);
1076
1077 normal_indices.push(0);
1078 normal_index_map.insert(0, 0);
1079 // We use the previous index. Not great a fallback but less prone to
1080 // cause issues. FIXME: we should probably check if the
1081 // data is per-vertex-per-face and if so calculate the
1082 // average from adjacent face vertices.
1083 } else {
1084 normal_indices.push(*normal_indices.last().unwrap());
1085 }
1086 } else {
1087 match normal_index_map.get(&vert.vn) {
1088 Some(&index) => normal_indices.push(index as _),
1089 None => {
1090 let vn = vert.vn;
1091
1092 if vn * 3 + 2 >= normal.len() {
1093 return Err(LoadError::FaceNormalOutOfBounds);
1094 }
1095
1096 mesh.normals.push(normal[vn * 3]);
1097 mesh.normals.push(normal[vn * 3 + 1]);
1098 mesh.normals.push(normal[vn * 3 + 2]);
1099
1100 let next = normal_index_map.len() as u32;
1101 normal_indices.push(next);
1102 normal_index_map.insert(vn, next);
1103 }
1104 }
1105 }
1106 }
1107
1108 Ok(())
1109}
1110
1111/// Export a list of faces to a mesh and return it, optionally converting quads
1112/// to tris.
1113fn export_faces_multi_index(
1114 pos: &[Float],
1115 v_color: &[Float],
1116 texcoord: &[Float],
1117 normal: &[Float],
1118 faces: &[Face],
1119 mat_id: Option<usize>,
1120 load_options: &LoadOptions,
1121) -> Result<Mesh, LoadError> {
1122 let mut index_map = HashMap::new();
1123 let mut normal_index_map = HashMap::new();
1124 let mut texcoord_index_map = HashMap::new();
1125
1126 let mut mesh = Mesh {
1127 material_id: mat_id,
1128 ..Default::default()
1129 };
1130
1131 let mut is_all_triangles = true;
1132
1133 for f in faces {
1134 // Optimized paths for Triangles and Quads, Polygon handles the general case of
1135 // an unknown length triangle fan
1136 match *f {
1137 Face::Point(ref a) => {
1138 if !load_options.ignore_points {
1139 add_vertex_multi_index(
1140 &mut mesh,
1141 &mut index_map,
1142 &mut normal_index_map,
1143 &mut texcoord_index_map,
1144 a,
1145 pos,
1146 v_color,
1147 texcoord,
1148 normal,
1149 )?;
1150 if load_options.triangulate {
1151 add_vertex_multi_index(
1152 &mut mesh,
1153 &mut index_map,
1154 &mut normal_index_map,
1155 &mut texcoord_index_map,
1156 a,
1157 pos,
1158 v_color,
1159 texcoord,
1160 normal,
1161 )?;
1162 add_vertex_multi_index(
1163 &mut mesh,
1164 &mut index_map,
1165 &mut normal_index_map,
1166 &mut texcoord_index_map,
1167 a,
1168 pos,
1169 v_color,
1170 texcoord,
1171 normal,
1172 )?;
1173 } else {
1174 is_all_triangles = false;
1175 mesh.face_arities.push(1);
1176 }
1177 }
1178 }
1179 Face::Line(ref a, ref b) => {
1180 if !load_options.ignore_lines {
1181 add_vertex_multi_index(
1182 &mut mesh,
1183 &mut index_map,
1184 &mut normal_index_map,
1185 &mut texcoord_index_map,
1186 a,
1187 pos,
1188 v_color,
1189 texcoord,
1190 normal,
1191 )?;
1192 add_vertex_multi_index(
1193 &mut mesh,
1194 &mut index_map,
1195 &mut normal_index_map,
1196 &mut texcoord_index_map,
1197 b,
1198 pos,
1199 v_color,
1200 texcoord,
1201 normal,
1202 )?;
1203 if load_options.triangulate {
1204 add_vertex_multi_index(
1205 &mut mesh,
1206 &mut index_map,
1207 &mut normal_index_map,
1208 &mut texcoord_index_map,
1209 b,
1210 pos,
1211 v_color,
1212 texcoord,
1213 normal,
1214 )?;
1215 } else {
1216 is_all_triangles = false;
1217 mesh.face_arities.push(2);
1218 }
1219 }
1220 }
1221 Face::Triangle(ref a, ref b, ref c) => {
1222 add_vertex_multi_index(
1223 &mut mesh,
1224 &mut index_map,
1225 &mut normal_index_map,
1226 &mut texcoord_index_map,
1227 a,
1228 pos,
1229 v_color,
1230 texcoord,
1231 normal,
1232 )?;
1233 add_vertex_multi_index(
1234 &mut mesh,
1235 &mut index_map,
1236 &mut normal_index_map,
1237 &mut texcoord_index_map,
1238 b,
1239 pos,
1240 v_color,
1241 texcoord,
1242 normal,
1243 )?;
1244 add_vertex_multi_index(
1245 &mut mesh,
1246 &mut index_map,
1247 &mut normal_index_map,
1248 &mut texcoord_index_map,
1249 c,
1250 pos,
1251 v_color,
1252 texcoord,
1253 normal,
1254 )?;
1255 if !load_options.triangulate {
1256 mesh.face_arities.push(3);
1257 }
1258 }
1259 Face::Quad(ref a, ref b, ref c, ref d) => {
1260 add_vertex_multi_index(
1261 &mut mesh,
1262 &mut index_map,
1263 &mut normal_index_map,
1264 &mut texcoord_index_map,
1265 a,
1266 pos,
1267 v_color,
1268 texcoord,
1269 normal,
1270 )?;
1271 add_vertex_multi_index(
1272 &mut mesh,
1273 &mut index_map,
1274 &mut normal_index_map,
1275 &mut texcoord_index_map,
1276 b,
1277 pos,
1278 v_color,
1279 texcoord,
1280 normal,
1281 )?;
1282 add_vertex_multi_index(
1283 &mut mesh,
1284 &mut index_map,
1285 &mut normal_index_map,
1286 &mut texcoord_index_map,
1287 c,
1288 pos,
1289 v_color,
1290 texcoord,
1291 normal,
1292 )?;
1293
1294 if load_options.triangulate {
1295 add_vertex_multi_index(
1296 &mut mesh,
1297 &mut index_map,
1298 &mut normal_index_map,
1299 &mut texcoord_index_map,
1300 a,
1301 pos,
1302 v_color,
1303 texcoord,
1304 normal,
1305 )?;
1306 add_vertex_multi_index(
1307 &mut mesh,
1308 &mut index_map,
1309 &mut normal_index_map,
1310 &mut texcoord_index_map,
1311 c,
1312 pos,
1313 v_color,
1314 texcoord,
1315 normal,
1316 )?;
1317 add_vertex_multi_index(
1318 &mut mesh,
1319 &mut index_map,
1320 &mut normal_index_map,
1321 &mut texcoord_index_map,
1322 d,
1323 pos,
1324 v_color,
1325 texcoord,
1326 normal,
1327 )?;
1328 } else {
1329 add_vertex_multi_index(
1330 &mut mesh,
1331 &mut index_map,
1332 &mut normal_index_map,
1333 &mut texcoord_index_map,
1334 d,
1335 pos,
1336 v_color,
1337 texcoord,
1338 normal,
1339 )?;
1340 is_all_triangles = false;
1341 mesh.face_arities.push(4);
1342 }
1343 }
1344 Face::Polygon(ref indices) => {
1345 if load_options.triangulate {
1346 let a = indices.first().ok_or(LoadError::InvalidPolygon)?;
1347 let mut b = indices.get(1).ok_or(LoadError::InvalidPolygon)?;
1348 for c in indices.iter().skip(2) {
1349 add_vertex_multi_index(
1350 &mut mesh,
1351 &mut index_map,
1352 &mut normal_index_map,
1353 &mut texcoord_index_map,
1354 a,
1355 pos,
1356 v_color,
1357 texcoord,
1358 normal,
1359 )?;
1360 add_vertex_multi_index(
1361 &mut mesh,
1362 &mut index_map,
1363 &mut normal_index_map,
1364 &mut texcoord_index_map,
1365 b,
1366 pos,
1367 v_color,
1368 texcoord,
1369 normal,
1370 )?;
1371 add_vertex_multi_index(
1372 &mut mesh,
1373 &mut index_map,
1374 &mut normal_index_map,
1375 &mut texcoord_index_map,
1376 c,
1377 pos,
1378 v_color,
1379 texcoord,
1380 normal,
1381 )?;
1382 b = c;
1383 }
1384 } else {
1385 for i in indices.iter() {
1386 add_vertex_multi_index(
1387 &mut mesh,
1388 &mut index_map,
1389 &mut normal_index_map,
1390 &mut texcoord_index_map,
1391 i,
1392 pos,
1393 v_color,
1394 texcoord,
1395 normal,
1396 )?;
1397 }
1398 is_all_triangles = false;
1399 mesh.face_arities.push(indices.len() as u32);
1400 }
1401 }
1402 }
1403 }
1404
1405 if is_all_triangles {
1406 // This is a triangle-only mesh.
1407 mesh.face_arities = Vec::new();
1408 }
1409
1410 #[cfg(feature = "merging")]
1411 if load_options.merge_identical_points {
1412 if !mesh.vertex_color.is_empty() {
1413 mesh.vertex_color_indices = mesh.indices.clone();
1414 merge_identical_points::<3>(&mut mesh.vertex_color, &mut mesh.vertex_color_indices);
1415 }
1416 merge_identical_points::<3>(&mut mesh.positions, &mut mesh.indices);
1417 merge_identical_points::<3>(&mut mesh.normals, &mut mesh.normal_indices);
1418 merge_identical_points::<2>(&mut mesh.texcoords, &mut mesh.texcoord_indices);
1419 }
1420
1421 #[cfg(feature = "reordering")]
1422 if load_options.reorder_data {
1423 reorder_data(&mut mesh);
1424 }
1425
1426 Ok(mesh)
1427}
1428
1429#[cfg(feature = "reordering")]
1430#[inline]
1431fn reorder_data(mesh: &mut Mesh) {
1432 // If we have per face per vertex data for UVs ...
1433 if mesh.positions.len() < mesh.texcoords.len() {
1434 mesh.texcoords = mesh
1435 .texcoord_indices
1436 .iter()
1437 .flat_map(|&index| {
1438 let index = index as usize * 2;
1439 IntoIterator::into_iter([mesh.texcoords[index], mesh.texcoords[index + 1]])
1440 })
1441 .collect::<Vec<_>>();
1442 } else {
1443 assert!(mesh.texcoords.len() == mesh.positions.len());
1444
1445 let mut new_texcoords = vec![0.0; mesh.positions.len()];
1446 mesh.texcoord_indices
1447 .iter()
1448 .zip(&mesh.indices)
1449 .for_each(|(&texcoord_index, &index)| {
1450 let texcoord_index = texcoord_index as usize * 2;
1451 let index = index as usize * 2;
1452 new_texcoords[index] = mesh.texcoords[texcoord_index];
1453 new_texcoords[index + 1] = mesh.texcoords[texcoord_index + 1];
1454 });
1455
1456 mesh.texcoords = new_texcoords;
1457 }
1458
1459 // Clear indices.
1460 mesh.texcoord_indices = Vec::new();
1461
1462 // If we have per face per vertex data for normals ...
1463 if mesh.positions.len() < mesh.normals.len() {
1464 mesh.normals = mesh
1465 .normal_indices
1466 .iter()
1467 .flat_map(|&index| {
1468 let index = index as usize * 2;
1469 IntoIterator::into_iter([
1470 mesh.normals[index],
1471 mesh.normals[index + 1],
1472 mesh.normals[index + 2],
1473 ])
1474 })
1475 .collect::<Vec<_>>();
1476 } else {
1477 assert!(mesh.normals.len() == mesh.positions.len());
1478
1479 let mut new_normals = vec![0.0; mesh.positions.len()];
1480 mesh.normal_indices
1481 .iter()
1482 .zip(&mesh.indices)
1483 .for_each(|(&normal_index, &index)| {
1484 let normal_index = normal_index as usize * 3;
1485 let index = index as usize * 3;
1486 new_normals[index] = mesh.normals[normal_index];
1487 new_normals[index + 1] = mesh.normals[normal_index + 1];
1488 new_normals[index + 2] = mesh.normals[normal_index + 2];
1489 });
1490
1491 mesh.normals = new_normals;
1492 }
1493
1494 // Clear indices.
1495 mesh.normal_indices = Vec::new();
1496}
1497
1498/// Merge identical points. A point has dimension N.
1499#[cfg(feature = "merging")]
1500#[inline]
1501fn merge_identical_points<const N: usize>(points: &mut Vec<Float>, indices: &mut Vec<u32>)
1502where
1503 [(); size_of::<[Float; N]>()]:,
1504{
1505 if indices.is_empty() {
1506 return;
1507 }
1508
1509 let mut compressed_indices = Vec::new();
1510 let mut canonical_indices = HashMap::<[u8; size_of::<[Float; N]>()], u32>::new();
1511
1512 let mut index = 0;
1513 *points = points
1514 .chunks(N)
1515 .filter_map(|position| {
1516 let position: &[Float; N] = &unsafe { *(position.as_ptr() as *const [Float; N]) };
1517
1518 // Ugly, but floats have no Eq and no Hash.
1519 let bitpattern = unsafe {
1520 std::mem::transmute::<&[Float; N], &[u8; size_of::<[Float; N]>()]>(position)
1521 };
1522
1523 match canonical_indices.get(bitpattern) {
1524 Some(&other_index) => {
1525 compressed_indices.push(other_index);
1526 None
1527 }
1528 None => {
1529 canonical_indices.insert(*bitpattern, index);
1530 compressed_indices.push(index);
1531 index += 1;
1532 Some(IntoIterator::into_iter(*position))
1533 }
1534 }
1535 })
1536 .flatten()
1537 .collect();
1538
1539 indices
1540 .iter_mut()
1541 .for_each(|vertex| *vertex = compressed_indices[*vertex as usize]);
1542}
1543
1544#[derive(Debug)]
1545struct TmpModels {
1546 models: Vec<Model>,
1547 pos: Vec<Float>,
1548 v_color: Vec<Float>,
1549 texcoord: Vec<Float>,
1550 normal: Vec<Float>,
1551 faces: Vec<Face>,
1552 // name of the current object being parsed
1553 name: String,
1554 // material used by the current object being parsed
1555 mat_id: Option<usize>,
1556}
1557
1558impl Default for TmpModels {
1559 #[inline]
1560 fn default() -> Self {
1561 Self {
1562 models: Vec::new(),
1563 pos: Vec::new(),
1564 v_color: Vec::new(),
1565 texcoord: Vec::new(),
1566 normal: Vec::new(),
1567 faces: Vec::new(),
1568 name: "unnamed_object".to_owned(),
1569 mat_id: None,
1570 }
1571 }
1572}
1573
1574impl TmpModels {
1575 #[inline]
1576 fn new() -> Self {
1577 Self::default()
1578 }
1579
1580 #[inline]
1581 fn pop_model(&mut self, load_options: &LoadOptions) -> Result<(), LoadError> {
1582 self.models.push(Model::new(
1583 if load_options.single_index {
1584 export_faces(
1585 &self.pos,
1586 &self.v_color,
1587 &self.texcoord,
1588 &self.normal,
1589 &self.faces,
1590 self.mat_id,
1591 load_options,
1592 )?
1593 } else {
1594 export_faces_multi_index(
1595 &self.pos,
1596 &self.v_color,
1597 &self.texcoord,
1598 &self.normal,
1599 &self.faces,
1600 self.mat_id,
1601 load_options,
1602 )?
1603 },
1604 self.name.clone(),
1605 ));
1606 self.faces.clear();
1607 Ok(())
1608 }
1609
1610 #[inline]
1611 fn into_models(self) -> Vec<Model> {
1612 self.models
1613 }
1614}
1615
1616#[derive(Debug)]
1617struct TmpMaterials {
1618 materials: Vec<Material>,
1619 mat_map: HashMap<String, usize>,
1620 mtlerr: Option<LoadError>,
1621}
1622
1623impl Default for TmpMaterials {
1624 #[inline]
1625 fn default() -> Self {
1626 Self {
1627 materials: Vec::new(),
1628 mat_map: HashMap::new(),
1629 mtlerr: None,
1630 }
1631 }
1632}
1633
1634impl TmpMaterials {
1635 #[inline]
1636 fn new() -> Self {
1637 Self::default()
1638 }
1639
1640 #[inline]
1641 fn push(&mut self, material: Material) {
1642 self.mat_map
1643 .insert(material.name.clone(), self.materials.len());
1644 self.materials.push(material);
1645 }
1646
1647 #[inline]
1648 fn merge(&mut self, mtl_load_result: MTLLoadResult) {
1649 match mtl_load_result {
1650 Ok((mut mats, map)) => {
1651 // Merge the loaded material lib with any currently loaded ones,
1652 // offsetting the indices of the appended
1653 // materials by our current length
1654 let mat_offset = self.materials.len();
1655 self.materials.append(&mut mats);
1656 for m in map {
1657 self.mat_map.insert(m.0, m.1 + mat_offset);
1658 }
1659 }
1660 Err(e) => {
1661 self.mtlerr = Some(e);
1662 }
1663 }
1664 }
1665
1666 #[inline]
1667 fn into_mtl_load_result(self) -> MTLLoadResult {
1668 Ok((self.materials, self.mat_map))
1669 }
1670
1671 #[inline]
1672 fn into_materials(self) -> Result<Vec<Material>, LoadError> {
1673 if !self.materials.is_empty() {
1674 Ok(self.materials)
1675 } else if let Some(mtlerr) = self.mtlerr {
1676 Err(mtlerr)
1677 } else {
1678 Ok(Vec::new())
1679 }
1680 }
1681}
1682
1683enum ParseReturnType {
1684 LoadMaterial(PathBuf),
1685 None,
1686}
1687
1688#[inline]
1689fn parse_obj_line(
1690 line: std::io::Result<String>,
1691 load_options: &LoadOptions,
1692 models: &mut TmpModels,
1693 materials: &TmpMaterials,
1694) -> Result<ParseReturnType, LoadError> {
1695 let (line, mut words) = match line {
1696 Ok(ref line) => (&line[..], line[..].split_whitespace()),
1697 Err(_e) => {
1698 #[cfg(feature = "log")]
1699 log::error!("load_obj - failed to read line due to {}", _e);
1700 return Err(LoadError::ReadError);
1701 }
1702 };
1703 match words.next() {
1704 Some("#") | None => Ok(ParseReturnType::None),
1705 Some("v") => {
1706 if !parse_floatn(&mut words, &mut models.pos, 3) {
1707 return Err(LoadError::PositionParseError);
1708 }
1709
1710 // Add inline vertex colors if present.
1711 parse_floatn(&mut words, &mut models.v_color, 3);
1712 Ok(ParseReturnType::None)
1713 }
1714 Some("vt") => {
1715 if !parse_floatn(&mut words, &mut models.texcoord, 2) {
1716 Err(LoadError::TexcoordParseError)
1717 } else {
1718 Ok(ParseReturnType::None)
1719 }
1720 }
1721 Some("vn") => {
1722 if !parse_floatn(&mut words, &mut models.normal, 3) {
1723 Err(LoadError::NormalParseError)
1724 } else {
1725 Ok(ParseReturnType::None)
1726 }
1727 }
1728 Some("f") | Some("l") => {
1729 if !parse_face(
1730 words,
1731 &mut models.faces,
1732 models.pos.len() / 3,
1733 models.texcoord.len() / 2,
1734 models.normal.len() / 3,
1735 ) {
1736 Err(LoadError::FaceParseError)
1737 } else {
1738 Ok(ParseReturnType::None)
1739 }
1740 }
1741 // Just treating object and group tags identically. Should there be different behavior
1742 // for them?
1743 Some("o") | Some("g") => {
1744 // If we were already parsing an object then a new object name
1745 // signals the end of the current one, so push it onto our list of objects
1746 if !models.faces.is_empty() {
1747 models.pop_model(load_options)?;
1748 }
1749 let size = line.chars().next().unwrap().len_utf8();
1750 models.name = line[size..].trim().to_owned();
1751 if models.name.is_empty() {
1752 models.name = "unnamed_object".to_owned();
1753 }
1754 Ok(ParseReturnType::None)
1755 }
1756 Some("mtllib") => {
1757 // File name can include spaces so we cannot rely on a SplitWhitespace iterator
1758 let mtllib = line.split_once(' ').unwrap_or_default().1.trim();
1759 let mat_file = Path::new(mtllib).to_path_buf();
1760 Ok(ParseReturnType::LoadMaterial(mat_file))
1761 }
1762 Some("usemtl") => {
1763 let mat_name = line.split_once(' ').unwrap_or_default().1.trim().to_owned();
1764
1765 if !mat_name.is_empty() {
1766 let new_mat = materials.mat_map.get(&mat_name).cloned();
1767 // As materials are returned per-model, a new material within an object
1768 // has to emit a new model with the same name but different material
1769 if models.mat_id != new_mat && !models.faces.is_empty() {
1770 models.pop_model(load_options)?;
1771 }
1772 if new_mat.is_none() {
1773 #[cfg(feature = "log")]
1774 log::warn!(
1775 "Object {} refers to unfound material: {}",
1776 models.name,
1777 mat_name
1778 );
1779 }
1780 models.mat_id = new_mat;
1781 Ok(ParseReturnType::None)
1782 } else {
1783 Err(LoadError::MaterialParseError)
1784 }
1785 }
1786 // Just ignore unrecognized characters
1787 Some(_) => Ok(ParseReturnType::None),
1788 }
1789}
1790
1791#[inline]
1792fn parse_mtl_line(
1793 line: std::io::Result<String>,
1794 materials: &mut TmpMaterials,
1795 mut cur_mat: Material,
1796) -> Result<Material, LoadError> {
1797 let (line, mut words) = match line {
1798 Ok(ref line) => (line.trim(), line[..].split_whitespace()),
1799 Err(_e) => {
1800 #[cfg(feature = "log")]
1801 log::error!("load_obj - failed to read line due to {}", _e);
1802 return Err(LoadError::ReadError);
1803 }
1804 };
1805
1806 match words.next() {
1807 Some("#") | None => {}
1808 Some("newmtl") => {
1809 // If we were passing a material save it out to our vector
1810 if !cur_mat.name.is_empty() {
1811 materials.push(cur_mat);
1812 }
1813 cur_mat = Material::default();
1814 cur_mat.name = line[6..].trim().to_owned();
1815 if cur_mat.name.is_empty() {
1816 return Err(LoadError::InvalidObjectName);
1817 }
1818 }
1819 Some("Ka") => cur_mat.ambient = Some(parse_float3(words)?),
1820 Some("Kd") => cur_mat.diffuse = Some(parse_float3(words)?),
1821 Some("Ks") => cur_mat.specular = Some(parse_float3(words)?),
1822 Some("Ke") => cur_mat.emissive = Some(parse_float3(words)?),
1823 Some("Ns") => cur_mat.shininess = Some(parse_float(words.next())?),
1824 Some("Ni") => cur_mat.optical_density = Some(parse_float(words.next())?),
1825 Some("d") => cur_mat.dissolve = Some(parse_float(words.next())?),
1826 Some("map_Ka") => match line.get(6..).map(str::trim) {
1827 Some("") | None => return Err(LoadError::MaterialParseError),
1828 Some(tex) => cur_mat.ambient_texture = Some(tex.to_owned()),
1829 },
1830 Some("map_Kd") => match line.get(6..).map(str::trim) {
1831 Some("") | None => return Err(LoadError::MaterialParseError),
1832 Some(tex) => cur_mat.diffuse_texture = Some(tex.to_owned()),
1833 },
1834 Some("map_Ks") => match line.get(6..).map(str::trim) {
1835 Some("") | None => return Err(LoadError::MaterialParseError),
1836 Some(tex) => cur_mat.specular_texture = Some(tex.to_owned()),
1837 },
1838 Some("map_Bump") | Some("map_bump") => match line.get(8..).map(str::trim) {
1839 Some("") | None => return Err(LoadError::MaterialParseError),
1840 Some(tex) => cur_mat.normal_texture = Some(tex.to_owned()),
1841 },
1842 Some("map_Ns") | Some("map_ns") | Some("map_NS") => match line.get(6..).map(str::trim) {
1843 Some("") | None => return Err(LoadError::MaterialParseError),
1844 Some(tex) => cur_mat.shininess_texture = Some(tex.to_owned()),
1845 },
1846 Some("bump") => match line.get(4..).map(str::trim) {
1847 Some("") | None => return Err(LoadError::MaterialParseError),
1848 Some(tex) => cur_mat.normal_texture = Some(tex.to_owned()),
1849 },
1850 Some("map_d") => match line.get(5..).map(str::trim) {
1851 Some("") | None => return Err(LoadError::MaterialParseError),
1852 Some(tex) => cur_mat.dissolve_texture = Some(tex.to_owned()),
1853 },
1854 Some("illum") => {
1855 if let Some(p) = words.next() {
1856 match FromStr::from_str(p) {
1857 Ok(x) => cur_mat.illumination_model = Some(x),
1858 Err(_) => return Err(LoadError::MaterialParseError),
1859 }
1860 } else {
1861 return Err(LoadError::MaterialParseError);
1862 }
1863 }
1864 Some(unknown) => {
1865 if !unknown.is_empty() {
1866 let param = line[unknown.len()..].trim().to_owned();
1867 cur_mat.unknown_param.insert(unknown.to_owned(), param);
1868 }
1869 }
1870 }
1871 Ok(cur_mat)
1872}
1873
1874/// Load the various objects specified in the `OBJ` file and any associated
1875/// `MTL` file.
1876///
1877/// Returns a pair of `Vec`s containing the loaded models and materials from the
1878/// file.
1879///
1880/// # Arguments
1881///
1882/// * `load_options` – Governs on-the-fly processing of the mesh during loading.
1883/// See [`LoadOptions`] for more information.
1884pub fn load_obj<P>(file_name: P, load_options: &LoadOptions) -> LoadResult
1885where
1886 P: AsRef<Path> + fmt::Debug,
1887{
1888 let file = match File::open(file_name.as_ref()) {
1889 Ok(f) => f,
1890 Err(_e) => {
1891 #[cfg(feature = "log")]
1892 log::error!("load_obj - failed to open {:?} due to {}", file_name, _e);
1893 return Err(LoadError::OpenFileFailed);
1894 }
1895 };
1896 let mut reader = BufReader::new(file);
1897 load_obj_buf(&mut reader, load_options, |mat_path| {
1898 let full_path = if let Some(parent) = file_name.as_ref().parent() {
1899 parent.join(mat_path)
1900 } else {
1901 mat_path.to_owned()
1902 };
1903
1904 self::load_mtl(full_path)
1905 })
1906}
1907
1908/// Load the materials defined in a `MTL` file.
1909///
1910/// Returns a pair with a `Vec` holding all loaded materials and a `HashMap`
1911/// containing a mapping of material names to indices in the Vec.
1912pub fn load_mtl<P>(file_name: P) -> MTLLoadResult
1913where
1914 P: AsRef<Path> + fmt::Debug,
1915{
1916 let file = match File::open(file_name.as_ref()) {
1917 Ok(f) => f,
1918 Err(_e) => {
1919 #[cfg(feature = "log")]
1920 log::error!("load_mtl - failed to open {:?} due to {}", file_name, _e);
1921 return Err(LoadError::OpenFileFailed);
1922 }
1923 };
1924 let mut reader = BufReader::new(file);
1925 load_mtl_buf(&mut reader)
1926}
1927
1928/// Load the various meshes in an `OBJ` buffer.
1929///
1930/// This could e.g. be a network stream, a text file already in memory etc.
1931///
1932/// # Arguments
1933///
1934/// You must pass a `material_loader` function, which will return a material
1935/// given a name.
1936///
1937/// A trivial material loader may just look at the file name and then call
1938/// `load_mtl_buf` with the in-memory MTL file source.
1939///
1940/// Alternatively it could pass an `MTL` file in memory to `load_mtl_buf` to
1941/// parse materials from some buffer.
1942///
1943/// * `load_options` – Governs on-the-fly processing of the mesh during loading.
1944/// See [`LoadOptions`] for more information.
1945///
1946/// # Example
1947/// The test for `load_obj_buf` includes the OBJ and MTL files as strings
1948/// and uses a `Cursor` to provide a `BufRead` interface on the buffer.
1949///
1950/// ```
1951/// use std::{env, fs::File, io::BufReader};
1952///
1953/// let dir = env::current_dir().unwrap();
1954/// let mut cornell_box_obj = dir.clone();
1955/// cornell_box_obj.push("obj/cornell_box.obj");
1956/// let mut cornell_box_file = BufReader::new(File::open(cornell_box_obj.as_path()).unwrap());
1957///
1958/// let mut cornell_box_mtl1 = dir.clone();
1959/// cornell_box_mtl1.push("obj/cornell_box.mtl");
1960///
1961/// let mut cornell_box_mtl2 = dir.clone();
1962/// cornell_box_mtl2.push("obj/cornell_box2.mtl");
1963///
1964/// let m = tobj::load_obj_buf(
1965/// &mut cornell_box_file,
1966/// &tobj::LoadOptions {
1967/// triangulate: true,
1968/// single_index: true,
1969/// ..Default::default()
1970/// },
1971/// |p| match p.file_name().unwrap().to_str().unwrap() {
1972/// "cornell_box.mtl" => {
1973/// let f = File::open(cornell_box_mtl1.as_path()).unwrap();
1974/// tobj::load_mtl_buf(&mut BufReader::new(f))
1975/// }
1976/// "cornell_box2.mtl" => {
1977/// let f = File::open(cornell_box_mtl2.as_path()).unwrap();
1978/// tobj::load_mtl_buf(&mut BufReader::new(f))
1979/// }
1980/// _ => unreachable!(),
1981/// },
1982/// );
1983/// ```
1984pub fn load_obj_buf<B, ML>(
1985 reader: &mut B,
1986 load_options: &LoadOptions,
1987 material_loader: ML,
1988) -> LoadResult
1989where
1990 B: BufRead,
1991 ML: Fn(&Path) -> MTLLoadResult,
1992{
1993 if !load_options.is_valid() {
1994 return Err(LoadError::InvalidLoadOptionConfig);
1995 }
1996
1997 let mut models = TmpModels::new();
1998 let mut materials = TmpMaterials::new();
1999
2000 for line in reader.lines() {
2001 let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
2002 match parse_return {
2003 ParseReturnType::LoadMaterial(mat_file) => {
2004 materials.merge(material_loader(mat_file.as_path()));
2005 }
2006 ParseReturnType::None => {}
2007 }
2008 }
2009
2010 // For the last object in the file we won't encounter another object name to
2011 // tell us when it's done, so if we're parsing an object push the last one
2012 // on the list as well
2013 models.pop_model(load_options)?;
2014
2015 Ok((models.into_models(), materials.into_materials()))
2016}
2017
2018/// Load the various materials in a `MTL` buffer.
2019pub fn load_mtl_buf<B: BufRead>(reader: &mut B) -> MTLLoadResult {
2020 let mut materials = TmpMaterials::new();
2021 // The current material being parsed
2022 let mut cur_mat = Material::default();
2023
2024 for line in reader.lines() {
2025 cur_mat = parse_mtl_line(line, &mut materials, cur_mat)?;
2026 }
2027
2028 // Finalize the last material we were parsing
2029 if !cur_mat.name.is_empty() {
2030 materials.push(cur_mat);
2031 }
2032
2033 materials.into_mtl_load_result()
2034}
2035
2036#[cfg(feature = "async")]
2037/// Load the various meshes in an `OBJ` buffer.
2038///
2039/// This could e.g. be a text file already in memory, a file loaded
2040/// asynchronously over the network etc.
2041///
2042/// <div class="warning">
2043///
2044/// This function is not fully async, as it does not use async reader objects. This means you
2045/// must either use a blocking reader object, which negates the point of async in the first place,
2046/// or you must asynchronously read the entire buffer into memory, and then give an in-memory reader
2047/// to this function, which is wasteful with memory and not terribly efficient.
2048///
2049/// Instead, it is recommended to use crate-specific feature flag support to enable support for
2050/// various third-party async readers. For example, you can enable the `tokio` feature flag to
2051/// use [tokio::load_obj_buf()].
2052///
2053/// </div>
2054///
2055/// # Arguments
2056///
2057/// You must pass a `material_loader` function, which will return a future
2058/// that loads a material given a name.
2059///
2060/// A trivial material loader may just look at the file name and then call
2061/// `load_mtl_buf` with the in-memory MTL file source.
2062///
2063/// Alternatively it could pass an `MTL` file in memory to `load_mtl_buf` to
2064/// parse materials from some buffer.
2065///
2066/// * `load_options` – Governs on-the-fly processing of the mesh during loading.
2067/// See [`LoadOptions`] for more information.
2068///
2069/// # Example
2070/// The test for `load_obj_buf` includes the OBJ and MTL files as strings
2071/// and uses a `Cursor` to provide a `BufRead` interface on the buffer.
2072///
2073/// ```
2074/// async {
2075/// use std::{env, fs::File, io::BufReader};
2076///
2077/// let dir = env::current_dir().unwrap();
2078/// let mut cornell_box_obj = dir.clone();
2079/// cornell_box_obj.push("obj/cornell_box.obj");
2080/// let mut cornell_box_file = BufReader::new(File::open(cornell_box_obj.as_path()).unwrap());
2081///
2082/// let m =
2083/// tobj::load_obj_buf_async(&mut cornell_box_file, &tobj::GPU_LOAD_OPTIONS, move |p| {
2084/// let dir_clone = dir.clone();
2085/// async move {
2086/// let mut cornell_box_mtl1 = dir_clone.clone();
2087/// cornell_box_mtl1.push("obj/cornell_box.mtl");
2088///
2089/// let mut cornell_box_mtl2 = dir_clone.clone();
2090/// cornell_box_mtl2.push("obj/cornell_box2.mtl");
2091///
2092/// match p.as_str() {
2093/// "cornell_box.mtl" => {
2094/// let f = File::open(cornell_box_mtl1.as_path()).unwrap();
2095/// tobj::load_mtl_buf(&mut BufReader::new(f))
2096/// }
2097/// "cornell_box2.mtl" => {
2098/// let f = File::open(cornell_box_mtl2.as_path()).unwrap();
2099/// tobj::load_mtl_buf(&mut BufReader::new(f))
2100/// }
2101/// _ => unreachable!(),
2102/// }
2103/// }
2104/// })
2105/// .await;
2106/// };
2107/// ```
2108#[deprecated(
2109 since = "4.0.3",
2110 note = "load_obj_buf_async is not fully async. Use futures/tokio feature flags instead"
2111)]
2112pub async fn load_obj_buf_async<B, ML, MLFut>(
2113 reader: &mut B,
2114 load_options: &LoadOptions,
2115 material_loader: ML,
2116) -> LoadResult
2117where
2118 B: BufRead,
2119 ML: Fn(String) -> MLFut,
2120 MLFut: Future<Output = MTLLoadResult>,
2121{
2122 if !load_options.is_valid() {
2123 return Err(LoadError::InvalidLoadOptionConfig);
2124 }
2125
2126 let mut models = TmpModels::new();
2127 let mut materials = TmpMaterials::new();
2128
2129 for line in reader.lines() {
2130 let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
2131 match parse_return {
2132 ParseReturnType::LoadMaterial(mat_file) => {
2133 match mat_file.into_os_string().into_string() {
2134 Ok(mat_file) => materials.merge(material_loader(mat_file).await),
2135 Err(_mat_file) => {
2136 #[cfg(feature = "log")]
2137 log::error!(
2138 "load_obj - material path contains invalid Unicode: {_mat_file:?}"
2139 );
2140 return Err(LoadError::ReadError);
2141 }
2142 }
2143 }
2144 ParseReturnType::None => {}
2145 }
2146 }
2147
2148 // For the last object in the file we won't encounter another object name to
2149 // tell us when it's done, so if we're parsing an object push the last one
2150 // on the list as well
2151 models.pop_model(load_options)?;
2152
2153 Ok((models.into_models(), materials.into_materials()))
2154}
2155
2156/// Optional module supporting async loading with `futures` traits.
2157///
2158/// The functions in this module are drop-in replacements for the standard non-async functions in
2159/// this crate, but tailored to use [futures](https://crates.io/crates/futures)
2160/// [AsyncRead](futures_lite::AsyncRead) traits.
2161///
2162/// While `futures` provides basic read/write async traits, it does *not* provide filesystem IO
2163/// implementations for these traits, so this module only contains `*_buf()` variants of this
2164/// crate's functions.
2165#[cfg(feature = "futures")]
2166pub mod futures {
2167 use super::*;
2168
2169 use futures_lite::{pin, AsyncBufRead, AsyncBufReadExt, StreamExt};
2170
2171 /// Asynchronously load the various meshes in an 'OBJ' buffer.
2172 ///
2173 /// This functions exactly like [crate::load_obj_buf()], but uses async read traits and an async
2174 /// `material_loader` function. See [crate::load_obj_buf()] for more.
2175 ///
2176 /// This is the [futures](https://crates.io/crates/futures) variant of `load_obj_buf()`; see
2177 /// [module-level](futures) documentation for more.
2178 ///
2179 /// # Examples
2180 /// ```
2181 /// use futures_lite::io::BufReader;
2182 ///
2183 /// const CORNELL_BOX_OBJ: &[u8] = include_bytes!("../obj/cornell_box.obj");
2184 /// const CORNELL_BOX_MTL1: &[u8] = include_bytes!("../obj/cornell_box.mtl");
2185 /// const CORNELL_BOX_MTL2: &[u8] = include_bytes!("../obj/cornell_box2.mtl");
2186 ///
2187 /// # async fn wrapper() {
2188 /// let m = tobj::futures::load_obj_buf(
2189 /// BufReader::new(CORNELL_BOX_OBJ),
2190 /// &tobj::LoadOptions {
2191 /// triangulate: true,
2192 /// single_index: true,
2193 /// ..Default::default()
2194 /// },
2195 /// |p| async move {
2196 /// match p.to_str().unwrap() {
2197 /// "cornell_box.mtl" => {
2198 /// let r = BufReader::new(CORNELL_BOX_MTL1);
2199 /// tobj::futures::load_mtl_buf(r).await
2200 /// }
2201 /// "cornell_box2.mtl" => {
2202 /// let r = BufReader::new(CORNELL_BOX_MTL2);
2203 /// tobj::futures::load_mtl_buf(r).await
2204 /// }
2205 /// _ => unreachable!(),
2206 /// }
2207 /// },
2208 /// ).await;
2209 /// # }
2210 /// ```
2211 pub async fn load_obj_buf<B, ML, MLFut>(
2212 reader: B,
2213 load_options: &LoadOptions,
2214 material_loader: ML,
2215 ) -> LoadResult
2216 where
2217 B: AsyncBufRead,
2218 ML: Fn(PathBuf) -> MLFut,
2219 MLFut: Future<Output = MTLLoadResult>,
2220 {
2221 if !load_options.is_valid() {
2222 return Err(LoadError::InvalidLoadOptionConfig);
2223 }
2224
2225 let mut models = TmpModels::new();
2226 let mut materials = TmpMaterials::new();
2227
2228 pin!(reader);
2229 let mut lines = reader.lines();
2230 while let Some(line) = lines.next().await {
2231 let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
2232 match parse_return {
2233 ParseReturnType::LoadMaterial(mat_file) => {
2234 materials.merge(material_loader(mat_file).await);
2235 }
2236 ParseReturnType::None => {}
2237 }
2238 }
2239
2240 // For the last object in the file we won't encounter another object name to
2241 // tell us when it's done, so if we're parsing an object push the last one
2242 // on the list as well
2243 models.pop_model(load_options)?;
2244
2245 Ok((models.into_models(), materials.into_materials()))
2246 }
2247
2248 /// Asynchronously load the various materials in a `MTL` buffer.
2249 ///
2250 /// This is the [futures](https://crates.io/crates/futures) variant of `load_mtl_buf()`; see
2251 /// [module-level](futures) documentation for more.
2252 pub async fn load_mtl_buf<B: AsyncBufRead>(reader: B) -> MTLLoadResult {
2253 let mut materials = TmpMaterials::new();
2254 // The current material being parsed
2255 let mut cur_mat = Material::default();
2256
2257 pin!(reader);
2258 let mut lines = reader.lines();
2259 while let Some(line) = lines.next().await {
2260 cur_mat = parse_mtl_line(line, &mut materials, cur_mat)?;
2261 }
2262
2263 // Finalize the last material we were parsing
2264 if !cur_mat.name.is_empty() {
2265 materials.push(cur_mat);
2266 }
2267
2268 materials.into_mtl_load_result()
2269 }
2270}
2271
2272/// Optional module supporting async loading with `tokio` traits.
2273///
2274/// The functions in this module are drop-in replacements for the standard non-async functions in
2275/// this crate, but tailored to use [tokio](https://crates.io/crates/tokio)
2276/// [AsyncRead](::tokio::io::AsyncRead) traits.
2277#[cfg(feature = "tokio")]
2278pub mod tokio {
2279 use super::*;
2280
2281 use ::tokio::fs::File;
2282 use ::tokio::io::{AsyncBufRead, AsyncBufReadExt, BufReader};
2283 use ::tokio::pin;
2284
2285 /// Load the various objects specified in the `OBJ` file and any associated `MTL` file.
2286 ///
2287 /// This functions exactly like [crate::load_obj()] but uses async filesystem logic. See
2288 /// [crate::load_obj()] for more.
2289 ///
2290 /// This is the [tokio](https://crates.io/crates/tokio) variant of `load_obj()`; see
2291 /// [module-level](tokio) documentation for more.
2292 pub async fn load_obj<P>(file_name: P, load_options: &LoadOptions) -> LoadResult
2293 where
2294 P: AsRef<Path> + fmt::Debug,
2295 {
2296 let file = match File::open(file_name.as_ref()).await {
2297 Ok(f) => f,
2298 Err(_e) => {
2299 #[cfg(feature = "log")]
2300 log::error!("load_obj - failed to open {:?} due to {}", file_name, _e);
2301 return Err(LoadError::OpenFileFailed);
2302 }
2303 };
2304 load_obj_buf(BufReader::new(file), load_options, |mat_path| {
2305 // This needs to be "copied" into this closure before moving it into the async one below
2306 let file_name: &Path = file_name.as_ref();
2307 let file_name = file_name.to_path_buf();
2308 async move {
2309 let full_path = if let Some(parent) = file_name.parent() {
2310 parent.join(mat_path)
2311 } else {
2312 mat_path
2313 };
2314
2315 load_mtl(full_path).await
2316 }
2317 })
2318 .await
2319 }
2320
2321 /// Load the materials defined in a `MTL` file.
2322 ///
2323 /// This functions exactly like [crate::load_mtl()] but uses async filesystem logic. See
2324 /// [crate::load_mtl()] for more.
2325 ///
2326 /// This is the [tokio](https://crates.io/crates/tokio) variant of `load_mtl()`; see
2327 /// [module-level](tokio) documentation for more.
2328 pub async fn load_mtl<P>(file_name: P) -> MTLLoadResult
2329 where
2330 P: AsRef<Path> + fmt::Debug,
2331 {
2332 let file = match File::open(file_name.as_ref()).await {
2333 Ok(f) => f,
2334 Err(_e) => {
2335 #[cfg(feature = "log")]
2336 log::error!("load_mtl - failed to open {:?} due to {}", file_name, _e);
2337 return Err(LoadError::OpenFileFailed);
2338 }
2339 };
2340 load_mtl_buf(BufReader::new(file)).await
2341 }
2342
2343 /// Asynchronously load the various meshes in an 'OBJ' buffer.
2344 ///
2345 /// This functions exactly like [crate::load_obj_buf()], but uses async read traits and an async
2346 /// `material_loader` function. See [crate::load_obj_buf()] for more.
2347 ///
2348 /// This is the [tokio](https://crates.io/crates/tokio) variant of `load_obj_buf()`; see
2349 /// [module-level](tokio) documentation for more.
2350 pub async fn load_obj_buf<B, ML, MLFut>(
2351 reader: B,
2352 load_options: &LoadOptions,
2353 material_loader: ML,
2354 ) -> LoadResult
2355 where
2356 B: AsyncBufRead,
2357 ML: Fn(PathBuf) -> MLFut,
2358 MLFut: Future<Output = MTLLoadResult>,
2359 {
2360 if !load_options.is_valid() {
2361 return Err(LoadError::InvalidLoadOptionConfig);
2362 }
2363
2364 let mut models = TmpModels::new();
2365 let mut materials = TmpMaterials::new();
2366
2367 pin!(reader);
2368 let mut lines = reader.lines();
2369 while let Some(line) = lines.next_line().await.transpose() {
2370 let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
2371 match parse_return {
2372 ParseReturnType::LoadMaterial(mat_file) => {
2373 materials.merge(material_loader(mat_file).await);
2374 }
2375 ParseReturnType::None => {}
2376 }
2377 }
2378
2379 // For the last object in the file we won't encounter another object name to
2380 // tell us when it's done, so if we're parsing an object push the last one
2381 // on the list as well
2382 models.pop_model(load_options)?;
2383
2384 Ok((models.into_models(), materials.into_materials()))
2385 }
2386
2387 /// Asynchronously load the various materials in a `MTL` buffer.
2388 ///
2389 /// This is the [tokio](https://crates.io/crates/tokio) variant of `load_mtl_buf()`; see
2390 /// [module-level](tokio) documentation for more.
2391 pub async fn load_mtl_buf<B: AsyncBufRead>(reader: B) -> MTLLoadResult {
2392 let mut materials = TmpMaterials::new();
2393 // The current material being parsed
2394 let mut cur_mat = Material::default();
2395
2396 pin!(reader);
2397 let mut lines = reader.lines();
2398 while let Some(line) = lines.next_line().await.transpose() {
2399 cur_mat = parse_mtl_line(line, &mut materials, cur_mat)?;
2400 }
2401
2402 // Finalize the last material we were parsing
2403 if !cur_mat.name.is_empty() {
2404 materials.push(cur_mat);
2405 }
2406
2407 materials.into_mtl_load_result()
2408 }
2409}