diff --git a/Cargo.toml b/Cargo.toml index 54d2a0d..b9b0757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ merging = [] reordering = [] async = [] arbitrary = ["arbitrary/derive"] +use_f64 = [] [dependencies] arbitrary = { version = "1.2.3", optional = true } @@ -33,6 +34,7 @@ log = { version = "0.4.17", optional = true } [dev-dependencies] tokio-test = "0.4.2" +float_eq = "1.0.1" [package.metadata.docs.rs] -features = ["log", "merging", "reordering", "async"] +features = ["log", "merging", "reordering", "async", "use_f64"] diff --git a/src/lib.rs b/src/lib.rs index 7607279..a15163d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ //! //! ## Flat Data //! -//! Values are stored packed as [`f32`]s in flat `Vec`s. +//! Values are stored packed as [`f32`]s (or [`f64`]s with the use_f64 feature) in flat `Vec`s. //! //! For example, the `positions` member of a `Mesh` will contain `[x, y, z, x, //! y, z, ...]` which you can then use however you like. @@ -193,6 +193,9 @@ //! * [`async`](load_obj_buf_async) – Adds support for async loading of obj files from a buffer, //! with an async material loader. Useful in environments that do not //! support blocking IO (e.g. WebAssembly). +//! +//! * ['use_f64'] - Uses double-precision (f64) instead of single-precision (f32) floating point +//! types #![cfg_attr(feature = "merging", allow(incomplete_features))] #![cfg_attr(feature = "merging", feature(generic_const_exprs))] @@ -208,6 +211,12 @@ use std::{ str::{FromStr, SplitWhitespace}, }; +#[cfg(feature = "use_f64")] +type Float = f64; + +#[cfg(not(feature = "use_f64"))] +type Float = f32; + #[cfg(feature = "async")] use std::future::Future; @@ -257,7 +266,7 @@ pub const OFFLINE_RENDERING_LOAD_OPTIONS: LoadOptions = LoadOptions { /// It is assumed that all meshes will at least have positions, but normals and /// texture coordinates are optional. If no normals or texture coordinates where /// found then the corresponding `Vec`s in the `Mesh` will be empty. Values are -/// stored packed as [`f32`]s in flat `Vec`s. +/// stored packed as [`f32`]s (or [`f64`]s with the use_f64 feature) in flat `Vec`s. /// /// For examples the `positions` member of a loaded mesh will contain `[x, y, z, /// x, y, z, ...]` which you can then use however you like. Indices are also @@ -307,25 +316,25 @@ pub const OFFLINE_RENDERING_LOAD_OPTIONS: LoadOptions = LoadOptions { pub struct Mesh { /// Flattened 3 component floating point vectors, storing positions of /// vertices in the mesh. - pub positions: Vec, + pub positions: Vec, /// Flattened 3 component floating point vectors, storing the color /// associated with the vertices in the mesh. /// /// Most meshes do not have vertex colors. If no vertex colors are specified /// this will be empty. - pub vertex_color: Vec, + pub vertex_color: Vec, /// Flattened 3 component floating point vectors, storing normals of /// vertices in the mesh. /// /// Not all meshes have normals. If no normals are specified this will /// be empty. - pub normals: Vec, + pub normals: Vec, /// Flattened 2 component floating point vectors, storing texture /// coordinates of vertices in the mesh. /// /// Not all meshes have texture coordinates. If no texture coordinates are /// specified this will be empty. - pub texcoords: Vec, + pub texcoords: Vec, /// Indices for vertices of each face. If loaded with /// [`triangulate`](LoadOptions::triangulate) set to `true` each face in the /// mesh is a triangle. @@ -581,21 +590,21 @@ pub struct Material { /// Material name as specified in the `MTL` file. pub name: String, /// Ambient color of the material. - pub ambient: [f32; 3], + pub ambient: [Float; 3], /// Diffuse color of the material. - pub diffuse: [f32; 3], + pub diffuse: [Float; 3], /// Specular color of the material. - pub specular: [f32; 3], + pub specular: [Float; 3], /// Material shininess attribute. Also called `glossiness`. - pub shininess: f32, + pub shininess: Float, /// Dissolve attribute is the alpha term for the material. Referred to as /// dissolve since that's what the `MTL` file format docs refer to it as. - pub dissolve: f32, + pub dissolve: Float, /// Optical density also known as index of refraction. Called /// `optical_density` in the `MTL` specc. Takes on a value between 0.001 /// and 10.0. 1.0 means light does not bend as it passes through /// the object. - pub optical_density: f32, + pub optical_density: Float, /// Name of the ambient texture file for the material. pub ambient_texture: String, /// Name of the diffuse texture file for the material. @@ -768,7 +777,7 @@ enum Face { /// Parse the float information from the words. Words is an iterator over the /// float strings. Returns `false` if parsing failed. -fn parse_floatn(val_str: &mut SplitWhitespace, vals: &mut Vec, n: usize) -> bool { +fn parse_floatn(val_str: &mut SplitWhitespace, vals: &mut Vec, n: usize) -> bool { let sz = vals.len(); for p in val_str.take(n) { match FromStr::from_str(p) { @@ -776,12 +785,12 @@ fn parse_floatn(val_str: &mut SplitWhitespace, vals: &mut Vec, n: usize) -> Err(_) => return false, } } - // Require that we found the desired number of f32s. + // Require that we found the desired number of floats. sz + n == vals.len() } /// Parse the float3 into the array passed, returns false if parsing failed -fn parse_float3(val_str: SplitWhitespace, vals: &mut [f32; 3]) -> bool { +fn parse_float3(val_str: SplitWhitespace, vals: &mut [Float; 3]) -> bool { for (i, p) in val_str.enumerate().take(3) { match FromStr::from_str(p) { Ok(x) => vals[i] = x, @@ -829,10 +838,10 @@ fn add_vertex( mesh: &mut Mesh, index_map: &mut HashMap, vert: &VertexIndices, - pos: &[f32], - v_color: &[f32], - texcoord: &[f32], - normal: &[f32], + pos: &[Float], + v_color: &[Float], + texcoord: &[Float], + normal: &[Float], ) -> Result<(), LoadError> { match index_map.get(vert) { Some(&i) => mesh.indices.push(i), @@ -881,10 +890,10 @@ fn add_vertex( /// Export a list of faces to a mesh and return it, optionally converting quads /// to tris. fn export_faces( - pos: &[f32], - v_color: &[f32], - texcoord: &[f32], - normal: &[f32], + pos: &[Float], + v_color: &[Float], + texcoord: &[Float], + normal: &[Float], faces: &[Face], mat_id: Option, load_options: &LoadOptions, @@ -987,10 +996,10 @@ fn add_vertex_multi_index( normal_index_map: &mut HashMap, texcoord_index_map: &mut HashMap, vert: &VertexIndices, - pos: &[f32], - v_color: &[f32], - texcoord: &[f32], - normal: &[f32], + pos: &[Float], + v_color: &[Float], + texcoord: &[Float], + normal: &[Float], ) -> Result<(), LoadError> { match index_map.get(&vert.v) { Some(&i) => mesh.indices.push(i), @@ -1113,10 +1122,10 @@ fn add_vertex_multi_index( /// Export a list of faces to a mesh and return it, optionally converting quads /// to tris. fn export_faces_multi_index( - pos: &[f32], - v_color: &[f32], - texcoord: &[f32], - normal: &[f32], + pos: &[Float], + v_color: &[Float], + texcoord: &[Float], + normal: &[Float], faces: &[Face], mat_id: Option, load_options: &LoadOptions, @@ -1500,26 +1509,27 @@ fn reorder_data(mesh: &mut Mesh) { /// Merge identical points. A point has dimension N. #[cfg(feature = "merging")] #[inline] -fn merge_identical_points(points: &mut Vec, indices: &mut Vec) +fn merge_identical_points(points: &mut Vec, indices: &mut Vec) where - [(); size_of::<[f32; N]>()]:, + [(); size_of::<[Float; N]>()]:, { if indices.is_empty() { return; } let mut compressed_indices = Vec::new(); - let mut canonical_indices = HashMap::<[u8; size_of::<[f32; N]>()], u32>::new(); + let mut canonical_indices = HashMap::<[u8; size_of::<[Float; N]>()], u32>::new(); let mut index = 0; *points = points .chunks(N) .filter_map(|position| { - let position: &[f32; N] = &unsafe { *(position.as_ptr() as *const [f32; N]) }; + let position: &[Float; N] = &unsafe { *(position.as_ptr() as *const [Float; N]) }; - // Ugly, but f32 has no Eq and no Hash. - let bitpattern = - unsafe { std::mem::transmute::<&[f32; N], &[u8; size_of::<[f32; N]>()]>(position) }; + // Ugly, but floats have no Eq and no Hash. + let bitpattern = unsafe { + std::mem::transmute::<&[Float; N], &[u8; size_of::<[Float; N]>()]>(position) + }; match canonical_indices.get(bitpattern) { Some(&other_index) => { diff --git a/src/tests.rs b/src/tests.rs index 0aefca1..dd5cb8e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,3 +1,4 @@ +use float_eq::assert_float_eq; use std::{ env, fs::File, @@ -10,6 +11,10 @@ const CORNELL_BOX_OBJ: &'static str = include_str!("../obj/cornell_box.obj"); const CORNELL_BOX_MTL1: &'static str = include_str!("../obj/cornell_box.mtl"); const CORNELL_BOX_MTL2: &'static str = include_str!("../obj/cornell_box2.mtl"); +// Set the tolerance for float comparison +use crate::Float; +const TOL: Float = 0.0000001; + #[test] fn simple_triangle() { let m = tobj::load_obj( @@ -34,7 +39,7 @@ fn simple_triangle() { // Verify each position is loaded properly let expect_pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]; - assert_eq!(mesh.positions, expect_pos); + assert_float_eq!(mesh.positions, expect_pos, r2nd_all <= TOL); // Verify the indices are loaded properly let expect_idx = vec![0, 1, 2]; assert_eq!(mesh.indices, expect_idx); @@ -67,14 +72,14 @@ fn simple_triangle_colored() { // Verify each position is loaded properly let expect_pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]; - assert_eq!(mesh.positions, expect_pos); + assert_float_eq!(mesh.positions, expect_pos, r2nd_all <= TOL); // Verify the indices are loaded properly let expect_idx = vec![0, 1, 2]; assert_eq!(mesh.indices, expect_idx); // Verify vertex colors are loaded let expect_vertex_color = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]; - assert_eq!(mesh.vertex_color, expect_vertex_color); + assert_float_eq!(mesh.vertex_color, expect_vertex_color, r2nd_all <= TOL); } #[test] @@ -109,7 +114,7 @@ fn simple_quad_colored_merge() { 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, ]; - assert_eq!(mesh.positions, expect_pos); + assert_float_eq!(mesh.positions, expect_pos, r2nd_all <= TOL); // Verify the indices are loaded properly let expect_idx = vec![0, 1, 2, 1, 2, 3]; assert_eq!(mesh.indices, expect_idx); @@ -123,7 +128,7 @@ fn simple_quad_colored_merge() { 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, ]; - assert_eq!(mesh.vertex_color, expect_vertex_color); + assert_float_eq!(mesh.vertex_color, expect_vertex_color, r2nd_all <= TOL); let expect_vertex_color_index = vec![0, 1, 2, 3, 2, 4]; assert_eq!(mesh.vertex_color_indices, expect_vertex_color_index); } @@ -152,7 +157,7 @@ fn empty_name_triangle() { // Verify each position is loaded properly let expect_pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]; - assert_eq!(mesh.positions, expect_pos); + assert_float_eq!(mesh.positions, expect_pos, r2nd_all <= TOL); // Verify the indices are loaded properly let expect_idx = vec![0, 1, 2]; assert_eq!(mesh.indices, expect_idx); @@ -182,7 +187,7 @@ fn test_lines() { // Verify each position is loaded properly let expect_pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]; - assert_eq!(mesh.positions, expect_pos); + assert_float_eq!(mesh.positions, expect_pos, r2nd_all <= TOL); // Verify the indices are loaded properly let expect_idx = vec![0, 1, 1, 2, 2, 0]; assert_eq!(mesh.indices, expect_idx); @@ -241,17 +246,17 @@ fn multiple_face_formats() { let quad_expect_pos = vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0]; let quad_expect_tex = vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0]; let quad_expect_idx = vec![0, 1, 2, 0, 2, 3]; - assert_eq!(quad.positions, quad_expect_pos); - assert_eq!(quad.texcoords, quad_expect_tex); + assert_float_eq!(quad.positions, quad_expect_pos, r2nd_all <= TOL); + assert_float_eq!(quad.texcoords, quad_expect_tex, r2nd_all <= TOL); assert_eq!(quad.indices, quad_expect_idx); assert_eq!(models[1].name, "Quad_face"); let quad_face = &models[1].mesh; let quad_expect_normals = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]; assert_eq!(quad_face.material_id, None); - assert_eq!(quad_face.positions, quad_expect_pos); - assert_eq!(quad_face.texcoords, quad_expect_tex); - assert_eq!(quad_face.normals, quad_expect_normals); + assert_float_eq!(quad_face.positions, quad_expect_pos, r2nd_all <= TOL); + assert_float_eq!(quad_face.texcoords, quad_expect_tex, r2nd_all <= TOL); + assert_float_eq!(quad_face.normals, quad_expect_normals, r2nd_all <= TOL); assert_eq!(quad_face.indices, quad_expect_idx); assert_eq!(models[2].name, "Tri_v_vn"); @@ -260,8 +265,8 @@ fn multiple_face_formats() { let tri_expect_normals = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]; let tri_expect_idx = vec![0, 1, 2]; assert_eq!(tri.material_id, None); - assert_eq!(tri.positions, tri_expect_pos); - assert_eq!(tri.normals, tri_expect_normals); + assert_float_eq!(tri.positions, tri_expect_pos, r2nd_all <= TOL); + assert_float_eq!(tri.normals, tri_expect_normals, r2nd_all <= TOL); assert_eq!(tri.indices, tri_expect_idx); assert!(tri.texcoords.is_empty()); } @@ -272,7 +277,6 @@ fn validate_cornell(models: Vec, mats: Vec) { let mesh = &models[0].mesh; assert_eq!(mesh.material_id, Some(0)); let expect_indices = vec![0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11]; - // Will this be an issue with floating point precision? let expect_verts = vec![ 552.799988, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 559.200012, 549.599976, 0.000000, 559.200012, 290.000000, 0.000000, 114.000000, 240.000000, @@ -281,7 +285,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 296.000000, 423.000000, 0.000000, 247.000000, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the light loaded properly assert_eq!(models[1].name, "light"); @@ -293,7 +297,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 548.000000, 332.000000, 213.000000, 548.000000, 227.000000, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the ceiling loaded properly assert_eq!(models[2].name, "ceiling"); @@ -304,7 +308,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 559.200012, 0.000000, 548.799988, 0.000000, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the back wall loaded properly assert_eq!(models[3].name, "back_wall"); @@ -315,7 +319,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 559.200012, 556.000000, 548.799988, 559.200012, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the green wall loaded properly assert_eq!(models[4].name, "green_wall"); @@ -326,7 +330,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 0.000000, 0.000000, 548.799988, 559.200012, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the red wall loaded properly assert_eq!(models[5].name, "red_wall"); @@ -337,7 +341,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 559.200012, 556.000000, 548.799988, 0.000000, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the short block loaded properly assert_eq!(models[6].name, "short_block"); @@ -358,7 +362,7 @@ fn validate_cornell(models: Vec, mats: Vec) { 272.000000, 82.000000, 165.000000, 225.000000, 82.000000, 0.000000, 225.000000, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify the tall block loaded properly assert_eq!(models[7].name, "tall_block"); @@ -380,14 +384,14 @@ fn validate_cornell(models: Vec, mats: Vec) { 0.000000, 247.000000, ]; assert_eq!(mesh.indices, expect_indices); - assert_eq!(mesh.positions, expect_verts); + assert_float_eq!(mesh.positions, expect_verts, r2nd_all <= TOL); // Verify white material loaded properly assert_eq!(mats[0].name, "white"); let mat = &mats[0]; - assert_eq!(mat.ambient, [0.0, 0.0, 0.0]); - assert_eq!(mat.diffuse, [1.0, 1.0, 1.0]); - assert_eq!(mat.specular, [0.0, 0.0, 0.0]); + assert_float_eq!(mat.ambient, [0.0, 0.0, 0.0], r2nd_all <= TOL); + assert_float_eq!(mat.diffuse, [1.0, 1.0, 1.0], r2nd_all <= TOL); + assert_float_eq!(mat.specular, [0.0, 0.0, 0.0], r2nd_all <= TOL); assert_eq!( mat.unknown_param.get("Ke").map(|s| s.as_ref()), Some("1 1 1") @@ -397,9 +401,9 @@ fn validate_cornell(models: Vec, mats: Vec) { // Verify red material loaded properly assert_eq!(mats[1].name, "red"); let mat = &mats[1]; - assert_eq!(mat.ambient, [0.0, 0.0, 0.0]); - assert_eq!(mat.diffuse, [1.0, 0.0, 0.0]); - assert_eq!(mat.specular, [0.0, 0.0, 0.0]); + assert_float_eq!(mat.ambient, [0.0, 0.0, 0.0], r2nd_all <= TOL); + assert_float_eq!(mat.diffuse, [1.0, 0.0, 0.0], r2nd_all <= TOL); + assert_float_eq!(mat.specular, [0.0, 0.0, 0.0], r2nd_all <= TOL); assert_eq!(mat.illumination_model, Some(2)); assert_eq!(mat.ambient_texture, "this ambient texture has spaces.jpg"); assert_eq!(mat.diffuse_texture, "this diffuse texture has spaces.jpg"); @@ -414,9 +418,9 @@ fn validate_cornell(models: Vec, mats: Vec) { // Verify blue material loaded properly assert_eq!(mats[2].name, "blue"); let mat = &mats[2]; - assert_eq!(mat.ambient, [0.0, 0.0, 0.0]); - assert_eq!(mat.diffuse, [0.0, 0.0, 1.0]); - assert_eq!(mat.specular, [0.0, 0.0, 0.0]); + assert_float_eq!(mat.ambient, [0.0, 0.0, 0.0], r2nd_all <= TOL); + assert_float_eq!(mat.diffuse, [0.0, 0.0, 1.0], r2nd_all <= TOL); + assert_float_eq!(mat.specular, [0.0, 0.0, 0.0], r2nd_all <= TOL); assert_eq!(mat.shininess, 10.0); assert_eq!(mat.unknown_param.len(), 1); assert_eq!( @@ -427,18 +431,18 @@ fn validate_cornell(models: Vec, mats: Vec) { // Verify light material loaded properly assert_eq!(mats[3].name, "light"); let mat = &mats[3]; - assert_eq!(mat.ambient, [20.0, 20.0, 20.0]); - assert_eq!(mat.diffuse, [1.0, 1.0, 1.0]); - assert_eq!(mat.specular, [0.0, 0.0, 0.0]); + assert_float_eq!(mat.ambient, [20.0, 20.0, 20.0], r2nd_all <= TOL); + assert_float_eq!(mat.diffuse, [1.0, 1.0, 1.0], r2nd_all <= TOL); + assert_float_eq!(mat.specular, [0.0, 0.0, 0.0], r2nd_all <= TOL); assert_eq!(mat.dissolve, 0.8); assert_eq!(mat.optical_density, 1.25); // Verify green material loaded properly assert_eq!(mats[4].name, "green"); let mat = &mats[4]; - assert_eq!(mat.ambient, [0.0, 0.0, 0.0]); - assert_eq!(mat.diffuse, [0.0, 1.0, 0.0]); - assert_eq!(mat.specular, [0.0, 0.0, 0.0]); + assert_float_eq!(mat.ambient, [0.0, 0.0, 0.0], r2nd_all <= TOL); + assert_float_eq!(mat.diffuse, [0.0, 1.0, 0.0], r2nd_all <= TOL); + assert_float_eq!(mat.specular, [0.0, 0.0, 0.0], r2nd_all <= TOL); assert_eq!(mat.ambient_texture, "dummy_texture.png"); assert_eq!(mat.diffuse_texture, "dummy_texture.png"); assert_eq!(mat.specular_texture, "dummy_texture.png");