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