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

ekarpp / lumo / 14235426927

03 Apr 2025 03:53AM UTC coverage: 72.653%. First build
14235426927

push

github

ekarpp
0.6.1

* Generalize thread pool
* Paralellize material parsing for scenes
* Implement variable wavelength index of refraction (and dispersion)
* Implement efficiency optimized russian roulette
* Add batching to samplers
* Restricts mediums to scene boundaries
* Fix cylinder bounding box
* Fix instance sample on normals
* Update github actions

290 of 821 new or added lines in 36 files covered. (35.32%)

6740 of 9277 relevant lines covered (72.65%)

39613243.93 hits per line

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

45.0
/src/parser.rs
1
use crate::{Vec2, Vec3, Image, Float, Normal, Point};
2
use crate::tracer::{
3
    Scene, Material, Texture,
4
    TriangleMesh, Face, Mesh, Spectrum
5
};
6
use std::fs::{ self, File };
7
use std::sync::Arc;
8
use std::io::{
9
    self, BufRead, BufReader, Result,
10
    Cursor, Read, Write,
11
};
12
use rustc_hash::FxHashMap;
13
use zip::ZipArchive;
14

15
/// .obj parser
16
mod obj;
17
/// .mtl parser
18
mod mtl;
19

20
const SCENE_DIR: &str = "./scenes/";
21

22
/*
23
 * BEWARE WHO ENTERS! HERE BE DRAGONS!
24
 */
25

26
/// Function to create io::Error
27
fn obj_error(message: &str) -> io::Error {
6✔
28
    io::Error::new(io::ErrorKind::InvalidData, message)
6✔
29
}
6✔
30

31
/// For .obj and .mtl parsers
32
fn parse_double(token: &str) -> Result<Float> {
136,512✔
33
    token
136,512✔
34
        .parse()
136,512✔
35
        .map_err(|_| obj_error("Could not parse double in file"))
136,512✔
36
}
136,512✔
37

38
/// For .obj and .mtl parsers
39
fn parse_vec2(tokens: &[&str]) -> Result<Vec2> {
27,882✔
40
    Ok(Vec2::new(
27,882✔
41
        parse_double(tokens[1])?,
27,882✔
42
        parse_double(tokens[2])?,
27,882✔
43
    ))
44
}
27,882✔
45

46
/// For .obj and .mtl parsers
47
fn parse_vec3(tokens: &[&str]) -> Result<Vec3> {
26,916✔
48
    Ok(Vec3::new(
26,916✔
49
        parse_double(tokens[1])?,
26,916✔
50
        parse_double(tokens[2])?,
26,916✔
51
        parse_double(tokens[3])?,
26,916✔
52
    ))
53
}
26,916✔
54

55
/// For .obj and .mtl parsers
56
fn parse_idx(token: &str, vec_len: usize) -> Result<usize> {
199,200✔
57
    token
199,200✔
58
        .parse::<i64>()
199,200✔
59
        .map(|idx| {
199,200✔
60
            if idx > 0 {
199,200✔
61
                (idx - 1) as usize
199,200✔
62
            } else {
63
                (vec_len as i64 + idx) as usize
×
64
            }
65
        })
199,200✔
66
        .map_err(|_| obj_error("Could not parse index in file"))
199,200✔
67
}
199,200✔
68

69
/* these thigs below could be optimized alot...
70
 * but its boring work for little? gain */
71

72
fn _get_url(url: &str, bytes: &mut Vec<u8>) -> Result<()> {
2✔
73
    let mut curl = curl::easy::Easy::new();
2✔
74
    curl.url(url)
2✔
75
        .map_err(|e| obj_error(e.description()))?;
2✔
76

77
    let mut transfer = curl.transfer();
2✔
78
    transfer.write_function(|data| { bytes.extend_from_slice(data); Ok(data.len()) })
43✔
79
        .map_err(|e| obj_error(e.description()))?;
2✔
80

81
    transfer.perform()
2✔
82
        .map_err(|e| obj_error(e.description()))?;
2✔
83

84
    Ok(())
2✔
85
}
2✔
86

87
/// Extracts file matching `re` from zip file in `bytes`
88
fn _extract_zip(bytes: &[u8], end_match: &str) -> Result<Vec<u8>> {
3✔
89
    println!("Reading .zip");
3✔
90
    let mut zip = ZipArchive::new(Cursor::new(bytes))?;
3✔
91
    let mut data = Vec::new();
3✔
92
    let end_match = end_match.to_lowercase();
3✔
93

94
    for i in 0..zip.len() {
9✔
95
        let mut file = zip.by_index(i)?;
9✔
96

97
        if file.name().to_lowercase().ends_with(&end_match) {
9✔
98
            if data.is_empty() {
3✔
99
                println!("Extracting \"{}\"", file.name());
3✔
100
                file.read_to_end(&mut data)?;
3✔
101
            } else {
102
                return Err(
×
103
                    obj_error(&format!("Found multiple {} in the archive", end_match))
×
104
                );
×
105
            }
106
        }
6✔
107
    }
108

109
    if data.is_empty() {
3✔
110
        Err(obj_error(&format!("Could not find {} in the archive", end_match)))
×
111
    } else {
112
        Ok(data)
3✔
113
    }
114
}
3✔
115

116
/// Loads `tex_name` from `zip` to an `Image`
117
fn _img_from_zip(zip: &[u8], tex_name: &str) -> Result<Image<Spectrum>> {
×
118
    let file_bytes = _extract_zip(zip, tex_name)?;
×
119
    println!("Decoding texture");
×
120
    Image::from_file(file_bytes.as_slice())
×
121
        .map_err(|decode_error| obj_error(&decode_error.to_string()))
×
122
}
×
123

124
/// Loads a .OBJ file at the given path
125
pub fn mesh_from_path(path: &str, material: Material) -> Result<Mesh> {
×
126
    println!("Loading .OBJ file \"{}\"", path);
×
127
    obj::load_file(File::open(path)?, material)
×
128
}
×
129

130
/// Loads .OBJ file from resource at an URL. Supports direct .OBJ files and
131
/// .OBJ files within a zip archive.
132
pub fn mesh_from_url(url: &str, material: Material) -> Result<Mesh> {
6✔
133
    let path = _check_cached(url)?;
6✔
134
    println!("Loading .OBJ from \"{}\"", &path);
6✔
135
    let mut bytes = fs::read(path)?;
6✔
136

137
    if url.ends_with(".zip") {
6✔
138
        println!("Found zip archive, searching for .OBJ files");
3✔
139
        bytes = _extract_zip(&bytes, ".obj")?;
3✔
140
    } else if !url.ends_with(".obj") {
3✔
141
        return Err(obj_error(
×
142
            "Bad URL, or at least does not end with .zip or .obj",
×
143
        ));
×
144
    }
3✔
145

146
    obj::load_file(bytes.as_slice(), material)
6✔
147
}
6✔
148

149
fn _check_cached(url: &str) -> Result<String> {
6✔
150
    if !fs::exists(SCENE_DIR)? {
6✔
151
        fs::create_dir(SCENE_DIR)?;
1✔
152
    }
5✔
153
    let fname = url.rsplit('/').next()
6✔
154
        .ok_or(obj_error("couldn't regex parse url"))?;
6✔
155
    let path = format!("{}{}", SCENE_DIR, fname);
6✔
156
    if !fs::exists(&path)? {
6✔
157
        println!("\"{}\" not found, downloading from \"{}\"", path, url);
2✔
158
        let mut resp = Vec::new();
2✔
159
        _get_url(url, &mut resp)?;
2✔
160
        let mut file = fs::File::create(&path)?;
2✔
161
        file.write_all(&resp)?;
2✔
162
    }
4✔
163

164
    Ok(path)
6✔
165
}
6✔
166

167
/// Loads `tex_name` from .zip at `url`. zip cached to `SCENE_DIR`
168
pub fn texture_from_url(url: &str, tex_name: &str) -> Result<Image<Spectrum>> {
×
169
    if !tex_name.ends_with(".png") {
×
170
        return Err(obj_error("Can only load .png files"));
×
171
    }
×
172
    if !url.ends_with(".zip") {
×
173
        return Err(obj_error("Can only extract textures from zip archives"));
×
174
    }
×
175

176
    let path = _check_cached(url)?;
×
177
    println!("Loading texture \"{}\" from \"{}\"", tex_name, path);
×
178

×
179
    _img_from_zip(&fs::read(path)?, tex_name)
×
180
}
×
181

182
/// Parses a whole scene from a .obj file specified by `name`
183
/// in a .zip archive at `url`. Cache `url` to `SCENE_DIR`
184
pub fn scene_from_url(
×
185
    url: &str,
×
186
    obj_name: &str,
×
187
    map_ks: bool,
×
188
    mtllib: Option<&str>,
×
189
    env_map: Option<(&str, Float)>
×
190
) -> Result<Scene> {
×
191
    if !url.ends_with(".zip") {
×
192
        return Err(obj_error("Can only load scenes from .zip"));
×
193
    }
×
194
    if !obj_name.ends_with(".obj") {
×
195
        return Err(obj_error("Can only parse .obj files"));
×
196
    }
×
197

198
    let path = _check_cached(url)?;
×
199

200
    scene_from_file(&path, obj_name, map_ks, mtllib, env_map)
×
201
}
×
202

203
/// Load a scene from zip file at `pth`
204
pub fn scene_from_file(
×
205
    path: &str,
×
206
    obj_name: &str,
×
207
    map_ks: bool,
×
208
    mtllib: Option<&str>,
×
209
    env_map: Option<(&str, Float)>
×
210
) -> Result<Scene> {
×
211
    println!("Loading scene \"{}\" from \"{}\"", obj_name, path);
×
NEW
212
    let zip_file = Arc::new(fs::read(path)?);
×
NEW
213
    let obj_bytes = _extract_zip(&zip_file, obj_name)?;
×
214

215
    // parse materials first
216
    let mut materials = Vec::new();
×
217
    let mut material_indices = FxHashMap::<String, usize>::default();
×
218

219
    if let Some(mtllib_name) = mtllib {
×
NEW
220
        let mtl_bytes = _extract_zip(&zip_file, mtllib_name)?;
×
221
        mtl::load_file(
×
222
            mtl_bytes.as_slice(),
×
223
            map_ks,
×
NEW
224
            Arc::clone(&zip_file),
×
225
            &mut materials,
×
226
            &mut material_indices,
×
227
        )?;
×
228
    }
×
229

230
    let reader = BufReader::new(obj_bytes.as_slice());
×
231
    for line in reader.lines() {
×
232
        let line = line?.trim().to_string();
×
233
        if line.starts_with('#') || line.is_empty() {
×
234
            continue;
×
235
        }
×
236
        let tokens: Vec<&str> = line.split_ascii_whitespace().collect();
×
237

×
238
        if tokens[0] == "mtllib" {
×
239
            let mtllib_name = tokens[1];
×
NEW
240
            let mtl_bytes = _extract_zip(&zip_file, mtllib_name)?;
×
241
            mtl::load_file(
×
242
                mtl_bytes.as_slice(),
×
243
                map_ks,
×
NEW
244
                Arc::clone(&zip_file),
×
245
                &mut materials,
×
246
                &mut material_indices,
×
247
            )?;
×
248
        }
×
249
    }
250

251
    let mut scene = obj::load_scene(
×
252
        obj_bytes.as_slice(),
×
253
        materials,
×
254
        material_indices,
×
255
    )?;
×
256

257
    if let Some((map_file, scale)) = env_map {
×
NEW
258
        let map_bytes = _extract_zip(&zip_file, map_file)?;
×
259
        scene.set_environment_map(
×
260
            Texture::Image(Image::from_hdri_bytes(map_bytes.as_slice())?), scale,
×
261
        );
262
    }
×
263

264
    Ok(scene)
×
265
}
×
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