• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ekarpp / lumo / 5843185550

pending completion
5843185550

push

github

ekarpp
0.3.0 (#26)

* Adds depth of field to cameras
* Implement perspective and orthographic cameras
* Adds .mtl file parsing
* Adds whole scene parsing from .obj files
* Implement watertight triangle intersection due to Woop et. al. 2013
* Implements robust floating point error checking to hit computations
  * Ray origins are robustly offset according to floating point error to be on the correct side of surfaces
* Triangles store indices to read vertex/normal/texture data from TriangleMesh
* Improve test coverage
* Implement bidirectional path tracing with multiple importance sampling
  * Implements needed sampling methods on objects
  * Implements camera sampling for perspective cameras
  * Refactors image with rendering in progress to `Film`

2049 of 2049 new or added lines in 45 files covered. (100.0%)

2146 of 4336 relevant lines covered (49.49%)

7281111.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.8
/src/parser.rs
1
use crate::Image;
2
use crate::tracer::{Scene, Material, Texture, TriangleMesh, Face, Mesh};
3
use glam::{DVec2, DVec3};
4
use std::fs::File;
5
use std::sync::Arc;
6
use std::io::{self, BufRead, BufReader, Result};
7
use std::io::{Cursor, Read, Seek, Write};
8
use std::collections::HashMap;
9
use zip::ZipArchive;
10
use regex::Regex;
11
use mtl::MtlConfig;
12

13
/// .obj parser
14
mod obj;
15
/// .mtl parser
16
mod mtl;
17

18
/// Function to create io::Error
19
fn obj_error(message: &str) -> io::Error {
×
20
    io::Error::new(io::ErrorKind::InvalidData, message)
×
21
}
×
22

23
/// For .obj and .mtl parsers
24
fn parse_double(token: &str) -> Result<f64> {
125,940✔
25
    token
125,940✔
26
        .parse()
125,940✔
27
        .map_err(|_| obj_error("Could not parse double in file"))
125,940✔
28
}
125,940✔
29

30
/// For .obj and .mtl parsers
31
fn parse_vec2(tokens: &[&str]) -> Result<DVec2> {
25,962✔
32
    Ok(DVec2::new(
25,962✔
33
        parse_double(tokens[1])?,
25,962✔
34
        parse_double(tokens[2])?,
25,962✔
35
    ))
36
}
25,962✔
37

38
/// For .obj and .mtl parsers
39
fn parse_vec3(tokens: &[&str]) -> Result<DVec3> {
24,672✔
40
    Ok(DVec3::new(
24,672✔
41
        parse_double(tokens[1])?,
24,672✔
42
        parse_double(tokens[2])?,
24,672✔
43
        parse_double(tokens[3])?,
24,672✔
44
    ))
45
}
24,672✔
46

47
/// For .obj and .mtl parsers
48
fn parse_idx(token: &str, vec_len: usize) -> Result<usize> {
193,440✔
49
    token
193,440✔
50
        .parse::<i32>()
193,440✔
51
        .map(|idx| {
193,440✔
52
            if idx > 0 {
193,440✔
53
                (idx - 1) as usize
193,440✔
54
            } else {
55
                (vec_len as i32 + idx) as usize
×
56
            }
57
        })
193,440✔
58
        .map_err(|_| obj_error("Could not parse index in file"))
193,440✔
59
}
193,440✔
60

61
/* these thigs below could be optimized alot...
62
 * but its boring work for little? gain */
63

64
fn _get_url(url: &str) -> Result<Vec<u8>> {
4✔
65
    let mut bytes = Vec::new();
4✔
66

4✔
67
    ureq::get(url)
4✔
68
        .call()
4✔
69
        .map_err(|_| obj_error("Error during HTTP, error parsing not implemented"))?
4✔
70
        .into_reader()
4✔
71
        .read_to_end(&mut bytes)?;
4✔
72

73
    Ok(bytes)
4✔
74
}
4✔
75

76
fn _extract_zip(bytes: Vec<u8>, re: Regex) -> Result<Vec<u8>> {
3✔
77
    println!("Reading .zip");
3✔
78
    let mut zip = ZipArchive::new(Cursor::new(bytes))?;
3✔
79
    let mut data = Vec::new();
3✔
80

81
    for i in 0..zip.len() {
9✔
82
        let mut file = zip.by_index(i)?;
9✔
83

84
        if re.is_match(file.name()) {
9✔
85
            println!("Extracting \"{}\"", file.name());
3✔
86
            file.read_to_end(&mut data)?;
3✔
87
            break;
3✔
88
        }
6✔
89
    }
90

91
    if data.is_empty() {
3✔
92
        Err(obj_error("Could not find file in the archive"))
×
93
    } else {
94
        Ok(data)
3✔
95
    }
96
}
3✔
97

98
fn _bytes_to_file(bytes: Vec<u8>) -> Result<File> {
4✔
99
    let mut tmp_file = tempfile::tempfile()?;
4✔
100

101
    tmp_file.write_all(&bytes)?;
4✔
102
    tmp_file.rewind()?;
4✔
103

104
    Ok(tmp_file)
4✔
105
}
4✔
106

107
/// Loads a .OBJ file at the given path
108
pub fn mesh_from_path(path: &str, material: Material) -> Result<Mesh> {
×
109
    println!("Loading .OBJ file \"{}\"", path);
×
110
    obj::load_file(File::open(path)?, material)
×
111
}
×
112

113
/// Loads .OBJ file from resource at an URL. Supports direct .OBJ files and
114
/// .OBJ files within a zip archive.
115
pub fn mesh_from_url(url: &str, material: Material) -> Result<Mesh> {
4✔
116
    println!("Loading .OBJ from \"{}\"", url);
4✔
117
    let mut bytes = _get_url(url)?;
4✔
118

119
    if url.ends_with(".zip") {
4✔
120
        println!("Found zip archive, searching for .OBJ files");
3✔
121
        bytes = _extract_zip(bytes, Regex::new(r".+\.obj$").unwrap())?;
3✔
122
    } else if !url.ends_with(".obj") {
1✔
123
        return Err(obj_error(
×
124
            "Bad URL, or at least does not end with .zip or .obj",
×
125
        ));
×
126
    }
1✔
127

128
    let obj_file = _bytes_to_file(bytes)?;
4✔
129
    obj::load_file(obj_file, material)
4✔
130
}
4✔
131

132
/// Loads `tex_name` from .zip at `url`
133
pub fn texture_from_url(url: &str, tex_name: &str) -> Result<Image> {
×
134
    if !tex_name.ends_with(".png") {
×
135
        return Err(obj_error("Can only load .png files"));
×
136
    }
×
137
    if !url.ends_with(".zip") {
×
138
        return Err(obj_error("Can only extract textures from zip archives"));
×
139
    }
×
140

×
141
    println!("Loading texture \"{}\" from \"{}\"", tex_name, url);
×
142

143
    let resp = _get_url(url)?;
×
144

145
    let file_bytes = _extract_zip(resp, Regex::new(tex_name).unwrap())?;
×
146

147
    let file = _bytes_to_file(file_bytes)?;
×
148

149
    println!("Decoding texture");
×
150
    Image::from_file(file)
×
151
        .map_err(|decode_error| obj_error(&decode_error.to_string()))
×
152
}
×
153

154
/// Parses a whole scene from a .obj file specified by `name`
155
/// in a .zip archive at `url`
156
#[allow(clippy::single_match)]
157
pub fn scene_from_url(url: &str, obj_name: &str) -> Result<Scene> {
×
158
    if !url.ends_with(".zip") {
×
159
        return Err(obj_error("Can only load scenes from .zip"));
×
160
    }
×
161
    if !obj_name.ends_with(".obj") {
×
162
        return Err(obj_error("Can only parse .obj files"));
×
163
    }
×
164

×
165
    println!("Loading scene \"{}\" from \"{}\"", obj_name, url);
×
166

167
    let resp = _get_url(url)?;
×
168

169
    let obj_bytes = _extract_zip(resp.clone(), Regex::new(obj_name).unwrap())?;
×
170

171
    let obj_file = _bytes_to_file(obj_bytes.clone())?;
×
172

173
    // parse materials first
174
    let mut materials = HashMap::<String, MtlConfig>::new();
×
175
    let reader = BufReader::new(obj_file);
×
176
    for line in reader.lines() {
×
177
        let line = line?.trim().to_string();
×
178
        if line.starts_with('#') || line.is_empty() {
×
179
            continue;
×
180
        }
×
181
        let tokens: Vec<&str> = line.split_ascii_whitespace().collect();
×
182

×
183
        match tokens[0] {
×
184
            "mtllib" => {
×
185
                let mtllib_name = tokens[1];
×
186
                let mtl_bytes = _extract_zip(resp.clone(), Regex::new(mtllib_name).unwrap())?;
×
187
                let mtl_file = _bytes_to_file(mtl_bytes)?;
×
188

189
                mtl::load_file(mtl_file, &mut materials)?;
×
190
            }
191
            _ => (),
×
192
        }
193
    }
194

195
    let obj_file = _bytes_to_file(obj_bytes)?;
×
196

197
    obj::load_scene(obj_file, materials)
×
198
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc