dot_vox/lib.rs
1//! Load [MagicaVoxel](https://ephtracy.github.io/) `.vox` files from Rust.
2
3use parser::parse_vox_file;
4use std::{fs::File, io::Read};
5
6#[macro_use]
7extern crate lazy_static;
8#[macro_use]
9extern crate log;
10
11#[cfg(test)]
12extern crate avow;
13
14mod dot_vox_data;
15mod model;
16mod palette;
17mod parser;
18mod scene;
19mod types;
20
21pub use types::Rotation;
22
23pub use dot_vox_data::DotVoxData;
24
25pub use parser::{Dict, Material};
26
27pub use model::Model;
28pub use model::Size;
29pub use model::Voxel;
30
31pub use scene::*;
32
33pub use palette::Color;
34pub use palette::DEFAULT_INDEX_MAP;
35pub use palette::DEFAULT_PALETTE;
36
37/// Loads the supplied [MagicaVoxel](https://ephtracy.github.io/) `.vox` file
38///
39/// Loads the supplied file, parses it, and returns a [`DotVoxData`] containing
40/// the version of the MagicaVoxel file, a `Vec<`[`Model`]`>` containing all
41/// [`Model`]s contained within the file, a `Vec<u32>` containing the palette
42/// information (RGBA), and a `Vec<`[`Material`]`>` containing all the
43/// specialized materials.
44///
45/// # Panics
46///
47/// No panics should occur with this library -- if you find one, please raise a
48/// [GitHub issue](https://github.com/dust-engine/dot_vox/issues) for it.
49///
50/// # Errors
51///
52/// All errors are strings, and should describe the issue that caused them to
53/// occur.
54///
55/// # Examples
56///
57/// Loading a file:
58///
59/// ```
60/// use dot_vox::*;
61///
62/// let result = load("src/resources/placeholder.vox");
63/// assert_eq!(
64/// result.unwrap(),
65/// DotVoxData {
66/// version: 150,
67/// models: vec!(Model {
68/// size: Size { x: 2, y: 2, z: 2 },
69/// voxels: vec!(
70/// Voxel {
71/// x: 0,
72/// y: 0,
73/// z: 0,
74/// i: 225
75/// },
76/// Voxel {
77/// x: 0,
78/// y: 1,
79/// z: 1,
80/// i: 215
81/// },
82/// Voxel {
83/// x: 1,
84/// y: 0,
85/// z: 1,
86/// i: 235
87/// },
88/// Voxel {
89/// x: 1,
90/// y: 1,
91/// z: 0,
92/// i: 5
93/// }
94/// )
95/// }),
96/// index_map: DEFAULT_INDEX_MAP.to_vec(),
97/// palette: DEFAULT_PALETTE.to_vec(),
98/// materials: (0..256)
99/// .into_iter()
100/// .map(|i| Material {
101/// id: i,
102/// properties: {
103/// let mut map = Dict::new();
104/// map.insert("_ior".to_owned(), "0.3".to_owned());
105/// map.insert("_spec".to_owned(), "0.5".to_owned());
106/// map.insert("_rough".to_owned(), "0.1".to_owned());
107/// map.insert("_type".to_owned(), "_diffuse".to_owned());
108/// map.insert("_weight".to_owned(), "1".to_owned());
109/// map
110/// }
111/// })
112/// .collect(),
113/// scenes: placeholder::SCENES.to_vec(),
114/// layers: placeholder::LAYERS.to_vec(),
115/// }
116/// );
117/// ```
118pub fn load(filename: &str) -> Result<DotVoxData, &'static str> {
119 match File::open(filename) {
120 Ok(mut f) => {
121 let mut buffer = Vec::new();
122 f.read_to_end(&mut buffer).expect("Unable to read file");
123 load_bytes(&buffer)
124 }
125 Err(_) => Err("Unable to load file"),
126 }
127}
128
129/// Parses the byte array as a .vox file.
130///
131/// Parses the byte array and returns a [`DotVoxData`] containing the version
132/// of the MagicaVoxel file, a `Vec<`[`Model`]`>` containing all `Model`s
133/// contained within the file, a `Vec<u32>` containing the palette information
134/// (RGBA), and a `Vec<`[`Material`]`>` containing all the specialized
135/// materials.
136///
137/// # Panics
138///
139/// No panics should occur with this library -- if you find one, please raise a
140/// [GitHub issue](https://github.com/dust-engine/dot_vox/issues) for it.
141///
142/// # Errors
143///
144/// All errors are strings, and should describe the issue that caused them to
145/// occur.
146///
147/// # Examples
148///
149/// Reading a byte array:
150///
151/// ```
152/// use dot_vox::*;
153///
154/// let result = load_bytes(include_bytes!("resources/placeholder.vox"));
155/// assert_eq!(
156/// result.unwrap(),
157/// DotVoxData {
158/// version: 150,
159/// models: vec!(Model {
160/// size: Size { x: 2, y: 2, z: 2 },
161/// voxels: vec!(
162/// Voxel {
163/// x: 0,
164/// y: 0,
165/// z: 0,
166/// i: 225
167/// },
168/// Voxel {
169/// x: 0,
170/// y: 1,
171/// z: 1,
172/// i: 215
173/// },
174/// Voxel {
175/// x: 1,
176/// y: 0,
177/// z: 1,
178/// i: 235
179/// },
180/// Voxel {
181/// x: 1,
182/// y: 1,
183/// z: 0,
184/// i: 5
185/// }
186/// )
187/// }),
188/// index_map: DEFAULT_INDEX_MAP.to_vec(),
189/// palette: DEFAULT_PALETTE.to_vec(),
190/// materials: (0..256)
191/// .into_iter()
192/// .map(|i| Material {
193/// id: i,
194/// properties: {
195/// let mut map = Dict::new();
196/// map.insert("_ior".to_owned(), "0.3".to_owned());
197/// map.insert("_spec".to_owned(), "0.5".to_owned());
198/// map.insert("_rough".to_owned(), "0.1".to_owned());
199/// map.insert("_type".to_owned(), "_diffuse".to_owned());
200/// map.insert("_weight".to_owned(), "1".to_owned());
201/// map
202/// }
203/// })
204/// .collect(),
205/// scenes: placeholder::SCENES.to_vec(),
206/// layers: placeholder::LAYERS.to_vec(),
207/// }
208/// );
209/// ```
210pub fn load_bytes(bytes: &[u8]) -> Result<DotVoxData, &'static str> {
211 match parse_vox_file(bytes) {
212 Ok((_, parsed)) => Ok(parsed),
213 Err(_) => Err("Not a valid MagicaVoxel .vox file"),
214 }
215}
216
217/// Data extracted from placeholder.vox for example and testing purposes
218pub mod placeholder {
219 use super::*;
220
221 lazy_static! {
222 /// Scenes extracted from placeholder.vox
223 pub static ref SCENES: Vec<SceneNode> = vec![
224 SceneNode::Transform {
225 attributes: Dict::new(),
226 frames: vec![Frame::default()], // Is this true?? Why empty dict? FIXME
227 child: 1,
228 layer_id: 4294967295
229 },
230 SceneNode::Group {
231 attributes: Dict::new(),
232 children: vec![2]
233 },
234 SceneNode::Transform {
235 attributes: Dict::new(),
236 frames: {
237 let mut map = Dict::new();
238 map.insert("_t".to_owned(), "0 0 1".to_owned());
239
240 vec![Frame::new(map)]
241 },
242 child: 3,
243 layer_id: 0
244 },
245 SceneNode::Shape {
246 attributes: Dict::new(),
247 models: vec![ShapeModel{
248 model_id: 0,
249 attributes: Dict::new()
250 }],
251 },
252 ];
253
254 /// Layers extracted from placeholder.vox
255 pub static ref LAYERS: Vec<Layer> = (0..8)
256 .map(|layer| Layer {
257 attributes: {
258 let mut map = Dict::new();
259 map.insert("_name".to_string(), layer.to_string());
260
261 map
262 },
263 })
264 .collect();
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use crate::palette::DEFAULT_INDEX_MAP;
271
272 use super::*;
273 use avow::vec;
274
275 lazy_static! {
276 static ref DEFAULT_MATERIALS: Vec<Material> = (0..256)
277 .into_iter()
278 .map(|i| Material {
279 id: i,
280 properties: {
281 let mut map = Dict::new();
282 map.insert("_ior".to_owned(), "0.3".to_owned());
283 map.insert("_spec".to_owned(), "0.5".to_owned());
284 map.insert("_rough".to_owned(), "0.1".to_owned());
285 map.insert("_type".to_owned(), "_diffuse".to_owned());
286 map.insert("_weight".to_owned(), "1".to_owned());
287 map
288 }
289 })
290 .collect();
291 }
292
293 fn placeholder(
294 palette: Vec<Color>,
295 materials: Vec<Material>,
296 scenes: Vec<SceneNode>,
297 layers: Vec<Layer>,
298 ) -> DotVoxData {
299 DotVoxData {
300 version: 150,
301 models: vec![Model {
302 size: Size { x: 2, y: 2, z: 2 },
303 voxels: vec![
304 Voxel {
305 x: 0,
306 y: 0,
307 z: 0,
308 i: 225,
309 },
310 Voxel {
311 x: 0,
312 y: 1,
313 z: 1,
314 i: 215,
315 },
316 Voxel {
317 x: 1,
318 y: 0,
319 z: 1,
320 i: 235,
321 },
322 Voxel {
323 x: 1,
324 y: 1,
325 z: 0,
326 i: 5,
327 },
328 ],
329 }],
330 index_map: DEFAULT_INDEX_MAP.to_vec(),
331 palette,
332 materials,
333 scenes,
334 layers,
335 }
336 }
337
338 fn compare_data(actual: DotVoxData, expected: DotVoxData) {
339 assert_eq!(actual.version, expected.version);
340 assert_eq!(actual.models.len(), expected.models.len());
341 actual
342 .models
343 .into_iter()
344 .zip(expected.models.into_iter())
345 .for_each(|(actual, expected)| {
346 assert_eq!(actual.size, expected.size);
347 vec::are_eq(actual.voxels, expected.voxels);
348 });
349 vec::are_eq(actual.palette, expected.palette);
350 vec::are_eq(actual.materials, expected.materials);
351 vec::are_eq(actual.scenes, expected.scenes);
352 vec::are_eq(actual.layers, expected.layers)
353 }
354
355 #[test]
356 fn valid_file_with_palette_is_read_successfully() {
357 let result = load("src/resources/placeholder.vox");
358 assert!(result.is_ok());
359 compare_data(
360 result.unwrap(),
361 placeholder(
362 DEFAULT_PALETTE.to_vec(),
363 DEFAULT_MATERIALS.to_vec(),
364 placeholder::SCENES.to_vec(),
365 placeholder::LAYERS.to_vec(),
366 ),
367 );
368 }
369
370 #[test]
371 fn not_present_file_causes_error() {
372 let result = load("src/resources/not_here.vox");
373 assert!(result.is_err());
374 assert_eq!(result.unwrap_err(), "Unable to load file");
375 }
376
377 #[test]
378 fn non_vox_file_causes_error() {
379 let result = load("src/resources/not_a.vox");
380 assert!(result.is_err());
381 assert_eq!(result.unwrap_err(), "Not a valid MagicaVoxel .vox file");
382 }
383
384 #[test]
385 fn can_parse_vox_file_with_palette() {
386 let bytes = include_bytes!("resources/placeholder.vox").to_vec();
387 let result = super::parse_vox_file(&bytes);
388 assert!(result.is_ok());
389 let (_, models) = result.unwrap();
390 compare_data(
391 models,
392 placeholder(
393 DEFAULT_PALETTE.to_vec(),
394 DEFAULT_MATERIALS.to_vec(),
395 placeholder::SCENES.to_vec(),
396 placeholder::LAYERS.to_vec(),
397 ),
398 );
399 }
400
401 #[test]
402 fn can_parse_vox_file_with_materials() {
403 let bytes = include_bytes!("resources/placeholder-with-materials.vox").to_vec();
404 let result = super::parse_vox_file(&bytes);
405 assert!(result.is_ok());
406 let (_, voxel_data) = result.unwrap();
407 let mut materials: Vec<Material> = DEFAULT_MATERIALS.to_vec();
408 materials[216] = Material {
409 id: 216,
410 properties: {
411 let mut map = Dict::new();
412 map.insert("_ior".to_owned(), "0.3".to_owned());
413 map.insert("_spec".to_owned(), "0.821053".to_owned());
414 map.insert("_rough".to_owned(), "0.389474".to_owned());
415 map.insert("_type".to_owned(), "_metal".to_owned());
416 map.insert("_plastic".to_owned(), "1".to_owned());
417 map.insert("_weight".to_owned(), "0.694737".to_owned());
418 map
419 },
420 };
421 compare_data(
422 voxel_data,
423 placeholder(
424 DEFAULT_PALETTE.to_vec(),
425 materials,
426 placeholder::SCENES.to_vec(),
427 placeholder::LAYERS.to_vec(),
428 ),
429 );
430 }
431
432 fn write_and_load(data: DotVoxData) {
433 let mut buffer = Vec::new();
434 let write_result = data.write_vox(&mut buffer);
435 assert!(write_result.is_ok());
436 let load_result = load_bytes(&buffer);
437 assert!(load_result.is_ok());
438 compare_data(load_result.unwrap(), data);
439 }
440
441 #[test]
442 fn can_write_vox_format_without_palette_nor_materials() {
443 write_and_load(placeholder(Vec::new(), Vec::new(), Vec::new(), Vec::new()));
444 }
445
446 #[test]
447 fn can_write_vox_format() {
448 write_and_load(placeholder(
449 DEFAULT_PALETTE.to_vec(),
450 DEFAULT_MATERIALS.to_vec(),
451 placeholder::SCENES.to_vec(),
452 placeholder::LAYERS.to_vec(),
453 ));
454 }
455}