diff --git a/Cargo.toml b/Cargo.toml index 1a0b4f7..3797bff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dot_vox" -version = "1.0.1" +version = "2.0.0" authors = ["David Edmonds "] description = "A Rust library for loading MagicaVoxel .vox files." license = "MIT" @@ -14,6 +14,7 @@ readme = "README.md" bitflags = "^1.0.0" byteorder = "^1.0" lazy_static = "^1.0" +log = "^0.4" nom = "^3.0" [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 2dd2f58..4b02714 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ extern crate byteorder; #[macro_use] extern crate lazy_static; #[macro_use] +extern crate log; +#[macro_use] extern crate nom; #[cfg(test)] @@ -14,6 +16,7 @@ extern crate avow; mod dot_vox_data; mod palette; +mod parser; mod material; mod model; @@ -24,39 +27,18 @@ pub use material::material_properties::MaterialProperties; pub use material::material_type::MaterialType; pub use model::Model; -pub use model::size::Size; -pub use model::voxel::Voxel; +pub use model::Size; +pub use model::Voxel; -pub use palette::DEFAULT_PALETTE; +use nom::IResult; -use material::extract_materials; -use model::extract_models; -use palette::extract_palette; +pub use palette::DEFAULT_PALETTE; -use nom::IResult::Done; -use nom::le_u32; +use parser::parse_vox_file; use std::fs::File; use std::io::Read; -const MAGIC_NUMBER: &'static str = "VOX "; - -named!(parse_vox_file <&[u8], DotVoxData>, do_parse!( - tag!(MAGIC_NUMBER) >> - version: le_u32 >> - take!(12) >> - models: extract_models >> - palette: opt_res!(extract_palette) >> - opt_res!(complete!(take!(4))) >> - materials: opt_res!(extract_materials) >> - (DotVoxData { - version: version, - models: models, - palette: palette.unwrap_or_else(|_| DEFAULT_PALETTE.to_vec()), - materials: materials.unwrap_or_else(|_| vec![]), - }) -)); - /// Loads the supplied MagicaVoxel .vox file /// /// Loads the supplied file, parses it, and returns a `DotVoxData` containing @@ -103,7 +85,7 @@ pub fn load(filename: &str) -> Result { let mut buffer = Vec::new(); f.read_to_end(&mut buffer).expect("Unable to read file"); match parse_vox_file(&buffer) { - Done(_, parsed) => Ok(parsed), + IResult::Done(_, parsed) => Ok(parsed), _ => Err("Not a valid MagicaVoxel .vox file"), } } @@ -119,12 +101,11 @@ mod tests { use byteorder::{ByteOrder, LittleEndian}; lazy_static! { - /// The default palette used by MagicaVoxel - this is supplied if no palette is included in the .vox file. - static ref MODIFIED_PALETTE: Vec = include_bytes!("resources/modified_palette.bytes") - .chunks(4) - .map(LittleEndian::read_u32) - .collect(); - } + static ref MODIFIED_PALETTE: Vec = include_bytes!("resources/modified_palette.bytes") + .chunks(4) + .map(LittleEndian::read_u32) + .collect(); + } fn placeholder(palette: Vec, materials: Vec) -> DotVoxData { DotVoxData { @@ -141,13 +122,17 @@ mod tests { }, ], palette: palette, - materials: materials + materials: materials, } } fn compare_data(actual: DotVoxData, expected: DotVoxData) { assert_eq!(actual.version, expected.version); - assert_eq!(actual.models, expected.models); + actual.models.into_iter().zip(expected.models.into_iter()) + .for_each(|(actual, expected)| { + assert_eq!(actual.size, expected.size); + vec::are_eq(actual.voxels, expected.voxels); + }); vec::are_eq(actual.palette, expected.palette); vec::are_eq(actual.materials, expected.materials); } diff --git a/src/material/mod.rs b/src/material/mod.rs index 7703a74..a892c2b 100644 --- a/src/material/mod.rs +++ b/src/material/mod.rs @@ -19,8 +19,7 @@ pub struct Material { pub properties: MaterialProperties, } -named!(parse_material <&[u8], Material>, do_parse!( - take!(12) >> +named!(pub parse_material <&[u8], Material>, do_parse!( id: le_u32 >> material_type: parse_material_type >> properties: parse_material_properties >> @@ -30,83 +29,3 @@ named!(parse_material <&[u8], Material>, do_parse!( properties: properties }) )); - -named!(pub extract_materials <&[u8], Vec >, do_parse!( - models: many0!(complete!(parse_material)) >> - (models) -)); - -#[cfg(test)] -mod tests { - use super::*; - use avow::vec; - - #[test] - fn can_parse_material_chunk() { - let bytes = include_bytes!("../resources/valid_material.bytes").to_vec(); - let result = super::parse_material(&bytes); - assert!(result.is_done()); - let (_, material) = result.unwrap(); - assert_eq!(248, material.id); - assert_eq!(MaterialType::Metal(1.0), material.material_type); - assert_eq!( - MaterialProperties { - plastic: Some(1.0), - roughness: Some(0.0), - specular: Some(1.0), - ior: Some(0.3), - power: Some(4.0), - glow: Some(0.589474), - ..Default::default() - }, - material.properties - ); - } - - #[test] - fn can_parse_multiple_materials() { - let bytes = include_bytes!("../resources/multi-materials.bytes").to_vec(); - let result = super::extract_materials(&bytes); - assert!(result.is_done()); - let (_, materials) = result.unwrap(); - vec::are_eq( - materials, - vec![ - Material { - id: 78, - material_type: MaterialType::Metal(1.0), - properties: MaterialProperties { - plastic: Some(0.0), - roughness: Some(0.1), - specular: Some(0.5), - ior: Some(0.3), - ..Default::default() - }, - }, - Material { - id: 84, - material_type: MaterialType::Metal(0.526316), - properties: MaterialProperties { - plastic: Some(0.0), - roughness: Some(0.252632), - specular: Some(0.736842), - ior: Some(0.3), - ..Default::default() - }, - }, - Material { - id: 248, - material_type: MaterialType::Glass(0.810526), - properties: MaterialProperties { - plastic: Some(0.0), - roughness: Some(0.189474), - specular: Some(0.5), - ior: Some(0.547368), - attenuation: Some(0.021053), - ..Default::default() - }, - }, - ], - ); - } -} diff --git a/src/model/voxel.rs b/src/model.rs similarity index 65% rename from src/model/voxel.rs rename to src/model.rs index 9e1a433..6757bd4 100644 --- a/src/model/voxel.rs +++ b/src/model.rs @@ -1,5 +1,27 @@ use nom::{le_u8, le_u32}; +/// A renderable voxel Model +#[derive(Debug, PartialEq)] +pub struct Model { + /// The size of the model in voxels + pub size: Size, + /// The voxels to be displayed. + pub voxels: Vec, +} + +/// The size of a model in voxels +/// +/// Indicates the size of the model in Voxels. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Size { + /// The width of the model in voxels. + pub x: u32, + /// The height of the model in voxels. + pub y: u32, + /// The depth of the model in voxels. + pub z: u32, +} + /// A Voxel /// /// A Voxel is a point in 3D space, with an indexed colour attached. @@ -30,6 +52,13 @@ impl Voxel { } } +named!(pub parse_size <&[u8], Size>, do_parse!( + x: le_u32 >> + y: le_u32 >> + z: le_u32 >> + (Size { x: x, y: y, z: z }) +)); + named!(parse_voxel <&[u8], Voxel>, do_parse!( x: le_u8 >> y: le_u8 >> @@ -39,26 +68,7 @@ named!(parse_voxel <&[u8], Voxel>, do_parse!( )); named!(pub parse_voxels <&[u8], Vec >, do_parse!( - take!(12) >> num_voxels: le_u32 >> voxels: many_m_n!(num_voxels as usize, num_voxels as usize, parse_voxel) >> (voxels) -)); - -#[cfg(test)] -mod tests { - use super::*; - use avow::vec; - - #[test] - fn can_parse_voxels_chunk() { - let bytes = include_bytes!("../resources/valid_voxels.bytes").to_vec(); - let result = super::parse_voxels(&bytes); - assert!(result.is_done()); - let (_, voxels) = result.unwrap(); - vec::are_eq( - voxels, - vec![Voxel::new(0, 12, 22, 225), Voxel::new(12, 23, 13, 225)], - ); - } -} +)); \ No newline at end of file diff --git a/src/model/mod.rs b/src/model/mod.rs deleted file mode 100644 index 5fea157..0000000 --- a/src/model/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -pub mod size; -pub mod voxel; - -use {Size, Voxel}; -use model::size::parse_size; -use model::voxel::parse_voxels; -use nom::le_u32; - -/// A renderable voxel Model -#[derive(Debug, PartialEq)] -pub struct Model { - /// The size of the model in voxels - pub size: Size, - /// The voxels to be displayed. - pub voxels: Vec, -} - -named!(parse_model <&[u8], Model>, do_parse!( - size: parse_size >> - voxels: parse_voxels >> - (Model { size: size, voxels: voxels }) -)); - -named!(parse_models <&[u8], Vec >, do_parse!( - take!(12) >> - model_count: le_u32 >> - models: many_m_n!(model_count as usize, model_count as usize, parse_model) >> - (models) -)); - -named!(pub extract_models <&[u8], Vec >, switch!(peek!(take!(4)), - b"PACK" => call!(parse_models) | - b"SIZE" => map!(parse_model, |m| vec!(m)) -)); - -//TODO add model tests here diff --git a/src/model/size.rs b/src/model/size.rs deleted file mode 100644 index 37d357b..0000000 --- a/src/model/size.rs +++ /dev/null @@ -1,44 +0,0 @@ -use nom::le_u32; - -/// The size of a model in voxels -/// -/// Indicates the size of the model in Voxels. -#[derive(Debug, PartialEq)] -pub struct Size { - /// The width of the model in voxels. - pub x: u32, - /// The height of the model in voxels. - pub y: u32, - /// The depth of the model in voxels. - pub z: u32, -} - -named!(pub parse_size <&[u8], Size>, do_parse!( - take!(12) >> - x: le_u32 >> - y: le_u32 >> - z: le_u32 >> - (Size { x: x, y: y, z: z }) -)); - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_parse_size_chunk() { - let bytes = include_bytes!("../resources/valid_size.bytes").to_vec(); - let result = super::parse_size(&bytes); - assert!(result.is_done()); - let (_, size) = result.unwrap(); - assert_eq!( - size, - Size { - x: 24, - y: 24, - z: 24, - } - ); - } -} diff --git a/src/palette.rs b/src/palette.rs index a7be70d..0333c31 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -11,23 +11,4 @@ lazy_static! { .collect(); } -named!(pub extract_palette <&[u8], Vec >, complete!(do_parse!( - take!(12) >> - colors: many_m_n!(255, 255, le_u32) >> - (colors) -))); - -#[cfg(test)] -mod tests { - use super::*; - use avow::vec; - - #[test] - fn can_parse_palette_chunk() { - let bytes = include_bytes!("resources/valid_palette.bytes").to_vec(); - let result = super::extract_palette(&bytes); - assert!(result.is_done()); - let (_, palette) = result.unwrap(); - vec::are_eq(palette, DEFAULT_PALETTE.to_vec()); - } -} +named!(pub extract_palette <&[u8], Vec >, many1!(le_u32)); diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..ef749d4 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,271 @@ +use {DEFAULT_PALETTE, DotVoxData, Material, material, Model, model, palette, Size, Voxel}; +use nom::{IResult, le_u32}; + +const MAGIC_NUMBER: &'static str = "VOX "; + +#[derive(Debug, PartialEq)] +pub enum Chunk { + Main(Vec), + Size(Size), + Voxels(Vec), + Pack(Model), + Palette(Vec), + Material(Material), + Unknown(String), + Invalid(Vec), +} + +named!(pub parse_vox_file <&[u8], DotVoxData>, do_parse!( + tag!(MAGIC_NUMBER) >> + version: le_u32 >> + main: parse_chunk >> + (map_chunk_to_data(version, main)) +)); + +fn map_chunk_to_data(version: u32, main: Chunk) -> DotVoxData { + match main { + Chunk::Main(children) => { + let mut size_holder: Option = None; + let mut models: Vec = vec![]; + let mut palette_holder: Vec = DEFAULT_PALETTE.to_vec(); + let mut materials: Vec = vec![]; + for chunk in children { + match chunk { + Chunk::Size(size) => size_holder = Some(size), + Chunk::Voxels(voxels) => { + if let Some(size) = size_holder { + models.push(Model { size, voxels }) + } + }, + Chunk::Pack(model) => models.push(model), + Chunk::Palette(palette) => palette_holder = palette, + Chunk::Material(material) => materials.push(material), + _ => error!("Unmapped chunk {:?}", chunk) + } + } + + DotVoxData { + version, + models, + palette: palette_holder, + materials, + } + }, + _ => DotVoxData { + version, + models: vec![], + palette: vec![], + materials: vec![], + } + } +} + +named!(parse_chunk <&[u8], Chunk>, do_parse!( + id: take_str!(4) >> + content_size: le_u32 >> + children_size: le_u32 >> + chunk_content: take!(content_size) >> + child_content: take!(children_size) >> + (build_chunk(id, chunk_content, children_size, child_content)) +)); + +fn build_chunk(id: &str, + chunk_content: &[u8], + children_size: u32, + child_content: &[u8]) -> Chunk { + if children_size == 0 { + match id { + "SIZE" => build_size_chunk(chunk_content), + "XYZI" => build_voxel_chunk(chunk_content), + "PACK" => build_pack_chunk(chunk_content), + "RGBA" => build_palette_chunk(chunk_content), + "MATT" => build_material_chunk(chunk_content), + _ => { + error!("Unknown childless chunk {:?}", id); + Chunk::Unknown(id.to_owned()) + } + } + } else { + let result: IResult<&[u8], Vec> = many0!(child_content, parse_chunk); + let child_chunks = match result { + IResult::Done(_, result) => result, + result => { + error!("Failed to parse child chunks, due to {:?}", result); + vec![] + } + }; + match id { + "MAIN" => Chunk::Main(child_chunks), + "PACK" => build_pack_chunk(chunk_content), + _ => { + error!("Unknown chunk with children {:?}", id); + Chunk::Unknown(id.to_owned()) + } + } + } +} + +fn build_material_chunk(chunk_content: &[u8]) -> Chunk { + if let IResult::Done(_, material) = material::parse_material(chunk_content) { + return Chunk::Material(material); + } + Chunk::Invalid(chunk_content.to_vec()) +} + +fn build_palette_chunk(chunk_content: &[u8]) -> Chunk { + if let IResult::Done(_, palette) = palette::extract_palette(chunk_content) { + return Chunk::Palette(palette) + } + Chunk::Invalid(chunk_content.to_vec()) +} + +fn build_pack_chunk(chunk_content: &[u8]) -> Chunk { + if let IResult::Done(chunk_content, Chunk::Size(size)) = parse_chunk(chunk_content) { + if let IResult::Done(_, Chunk::Voxels(voxels)) = parse_chunk(chunk_content) { + return Chunk::Pack(Model { size, voxels: voxels.to_vec() }) + } + } + Chunk::Invalid(chunk_content.to_vec()) +} + +fn build_size_chunk(chunk_content: &[u8]) -> Chunk { + match model::parse_size(chunk_content) { + IResult::Done(_, size) => Chunk::Size(size), + _ => Chunk::Invalid(chunk_content.to_vec()) + } +} + +fn build_voxel_chunk(chunk_content: &[u8]) -> Chunk { + match model::parse_voxels(chunk_content) { + IResult::Done(_, voxels) => Chunk::Voxels(voxels), + _ => Chunk::Invalid(chunk_content.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use avow::vec; + use {MaterialType, MaterialProperties}; + + #[test] + fn can_parse_size_chunk() { + let bytes = include_bytes!("resources/valid_size.bytes").to_vec(); + let result = parse_chunk(&bytes); + assert!(result.is_done()); + let (_, size) = result.unwrap(); + assert_eq!( + size, + Chunk::Size(Size { + x: 24, + y: 24, + z: 24, + }) + ); + } + + #[test] + fn can_parse_voxels_chunk() { + let bytes = include_bytes!("resources/valid_voxels.bytes").to_vec(); + let result = parse_chunk(&bytes); + assert!(result.is_done()); + let (_, voxels) = result.unwrap(); + match voxels { + Chunk::Voxels(voxels) => vec::are_eq( + voxels, + vec![Voxel::new(0, 0, 0, 225), + Voxel::new(0, 1, 1, 215), + Voxel::new(1, 0, 1, 235), + Voxel::new(1, 1, 0, 5), + ], + ), + chunk => panic!("Expecting Voxel chunk, got {:?}", chunk) + }; + } + + #[test] + fn can_parse_palette_chunk() { + let bytes = include_bytes!("resources/valid_palette.bytes").to_vec(); + let result = parse_chunk(&bytes); + assert!(result.is_done()); + let (_, palette) = result.unwrap(); + match palette { + Chunk::Palette(palette) => vec::are_eq(palette, DEFAULT_PALETTE.to_vec()), + chunk => panic!("Expecting Palette chunk, got {:?}", chunk) + }; + } + + #[test] + fn can_parse_material_chunk() { + let bytes = include_bytes!("resources/valid_material.bytes").to_vec(); + let result = parse_chunk(&bytes); + assert!(result.is_done()); + let (_, material) = result.unwrap(); + match material { + Chunk::Material(material) => { + assert_eq!(248, material.id); + assert_eq!(MaterialType::Metal(1.0), material.material_type); + assert_eq!( + MaterialProperties { + plastic: Some(1.0), + roughness: Some(0.0), + specular: Some(1.0), + ior: Some(0.3), + power: Some(4.0), + glow: Some(0.589474), + ..Default::default() + }, + material.properties + ); + }, + chunk => panic!("Expecting Material chunk, got {:?}", chunk) + }; + } + +// #[test] +// fn can_parse_multiple_materials() { +// let bytes = include_bytes!("resources/multi-materials.bytes").to_vec(); +// let result = parse_chunk(&bytes); +// assert!(result.is_done()); +// let (_, materials) = result.unwrap(); +// vec::are_eq( +// materials, +// vec![ +// Material { +// id: 78, +// material_type: MaterialType::Metal(1.0), +// properties: MaterialProperties { +// plastic: Some(0.0), +// roughness: Some(0.1), +// specular: Some(0.5), +// ior: Some(0.3), +// ..Default::default() +// }, +// }, +// Material { +// id: 84, +// material_type: MaterialType::Metal(0.526316), +// properties: MaterialProperties { +// plastic: Some(0.0), +// roughness: Some(0.252632), +// specular: Some(0.736842), +// ior: Some(0.3), +// ..Default::default() +// }, +// }, +// Material { +// id: 248, +// material_type: MaterialType::Glass(0.810526), +// properties: MaterialProperties { +// plastic: Some(0.0), +// roughness: Some(0.189474), +// specular: Some(0.5), +// ior: Some(0.547368), +// attenuation: Some(0.021053), +// ..Default::default() +// }, +// }, +// ], +// ); +// } +} diff --git a/src/resources/default_palette.bytes b/src/resources/default_palette.bytes index e29baed..6c281de 100644 Binary files a/src/resources/default_palette.bytes and b/src/resources/default_palette.bytes differ diff --git a/src/resources/modified_palette.bytes b/src/resources/modified_palette.bytes index 4dae70f..c31a0e9 100644 Binary files a/src/resources/modified_palette.bytes and b/src/resources/modified_palette.bytes differ diff --git a/src/resources/valid_voxels.bytes b/src/resources/valid_voxels.bytes index 5452d43..1c54b05 100644 Binary files a/src/resources/valid_voxels.bytes and b/src/resources/valid_voxels.bytes differ