commit d68d835baf94c84e12e85a85c1fc5a0b9551f86b Author: _ <> Date: Mon May 25 18:16:03 2020 +0000 :tada: Rust crate for parsing Inter Quake Models diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0dc7fdd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "inter_quake_model" +version = "0.1.0" +authors = ["_"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +byteorder = "1" +iota = "0.2" diff --git a/airplane.iqm b/airplane.iqm new file mode 100644 index 0000000..fb5a60a Binary files /dev/null and b/airplane.iqm differ diff --git a/arrow.iqm b/arrow.iqm new file mode 100644 index 0000000..c31f89f Binary files /dev/null and b/arrow.iqm differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e28e614 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,336 @@ +#[macro_use] +extern crate iota; + +use std::convert::TryInto; +use std::io::Cursor; +use std::mem::size_of; + +use byteorder::{ByteOrder, LittleEndian, ReadBytesExt}; + +pub mod consts { +use iota::iota; +iota! { + pub const VERSION: usize = iota; + , FILESIZE + , FLAGS + , NUM_TEXT + , OFS_TEXT + , NUM_MESHES + , OFS_MESHES + , NUM_VERTEXARRAYS + , NUM_VERTEXES + , OFS_VERTEXARRAYS + , NUM_TRIANGLES + , OFS_TRIANGLES + , OFS_ADJACENCY + , NUM_JOINTS + , OFS_JOINTS + , NUM_POSES + , OFS_POSES + , NUM_ANIMS + , OFS_ANIMS + , NUM_FRAMES + , NUM_FRAMECHANNELS + , OFS_FRAMES + , OFS_BOUNDS + , NUM_COMMENT + , OFS_COMMENT + , NUM_EXTENSIONS + , OFS_EXTENSIONS +} +} + +pub mod types { +iota! { + pub const POSITION: usize = iota; + , TEXCOORD + , NORMAL + , TANGENT + , BLENDINDEXES + , BLENDWEIGHTS + , COLOR +} + pub const CUSTOM: usize = 0x10; +} + +pub mod formats { +iota! { + pub const BYTE: u32 = iota; + , UBYTE + , SHORT + , USHORT + , INT + , UINT + , HALF + , FLOAT + , DOUBLE +} +} + +#[derive (Debug, Default)] +pub struct Mesh { + pub name: u32, + pub material: u32, + pub first_vertex: u32, + pub num_vertexes: u32, + pub first_triangle: u32, + pub num_triangles: u32, +} + +#[derive (Debug, Default)] +pub struct Header { + pub fields: [u32; 27], +} + +#[derive (Debug, Default)] +pub struct VertexArray { + va_type: u32, + va_flags: u32, + va_format: u32, + va_size: u32, + va_offset: u32, +} + +#[derive (Debug)] +pub struct Model <'a> { + data: &'a [u8], + + pub header: Header, + text: Vec , + pub meshes: Vec , + vertexarrays: Vec , +} + +#[derive (Debug)] +pub enum ModelLoadErr { + BadMagic, + UnsupportedVersion, + ParseMeshFailed, + ParseVertexArrayFailed, +} + +impl Header { + pub fn from_slice (input: &[u8]) -> Result { + let magic = b"INTERQUAKEMODEL\0"; + if &input [0..magic.len ()] != magic { + return Err (ModelLoadErr::BadMagic); + } + let input = &input [magic.len ()..]; + + let mut header = Header::default (); + LittleEndian::read_u32_into (&input [0..header.fields.len () * size_of:: ()], &mut header.fields); + + if header.fields [consts::VERSION] != 2 { + return Err (ModelLoadErr::UnsupportedVersion); + } + + Ok (header) + } +} + +impl Mesh { + pub fn from_slice (input: &[u8]) -> Result { + let mut mesh = Mesh::default (); + + let mut rdr = Cursor::new (input); + + for field in [ + &mut mesh.name, + &mut mesh.material, + &mut mesh.first_vertex, + &mut mesh.num_vertexes, + &mut mesh.first_triangle, + &mut mesh.num_triangles, + ].iter_mut () { + **field = rdr.read_u32:: ().map_err (|_| ModelLoadErr::ParseMeshFailed)?; + } + + Ok (mesh) + } +} + +impl VertexArray { + pub fn from_slice (input: &[u8]) -> Result { + let mut va = VertexArray::default (); + + let mut rdr = Cursor::new (input); + + for field in [ + &mut va.va_type, + &mut va.va_flags, + &mut va.va_format, + &mut va.va_size, + &mut va.va_offset, + ].iter_mut () { + **field = rdr.read_u32:: ().map_err (|_| ModelLoadErr::ParseVertexArrayFailed)?; + } + + Ok (va) + } +} + +impl <'a> Model <'a> { + pub fn from_slice (data: &'a [u8]) -> Result , ModelLoadErr> { + let header = Header::from_slice (data)?; + + let text = { + let offset: usize = header.fields [consts::OFS_TEXT].try_into ().unwrap (); + let num: usize = header.fields [consts::NUM_TEXT].try_into ().unwrap (); + Vec::from (&data [offset..offset + num]) + }; + + let meshes = { + let num: usize = header.fields [consts::NUM_MESHES].try_into ().unwrap (); + let mut meshes = Vec::with_capacity (num); + let mesh_size = 6 * 4; + let meshes_offset: usize = header.fields [consts::OFS_MESHES].try_into ().unwrap (); + + for i in 0..num { + let offset = meshes_offset + i * mesh_size; + let mesh_slice = &data [offset..offset + mesh_size]; + + let mesh = Mesh::from_slice (mesh_slice)?; + + meshes.push (mesh); + } + meshes + }; + + let vertexarrays = { + let num: usize = header.fields [consts::NUM_VERTEXARRAYS].try_into ().unwrap (); + let mut vertexarrays = Vec::with_capacity (num); + let vertexarray_size = 5 * 4; + let vertexarrays_offset: usize = header.fields [consts::OFS_VERTEXARRAYS].try_into ().unwrap (); + + for i in 0..num { + let offset = vertexarrays_offset + i * vertexarray_size; + let vertexarray_slice = &data [offset..offset + vertexarray_size]; + + let vertexarray = VertexArray::from_slice (vertexarray_slice)?; + + vertexarrays.push (vertexarray); + } + + vertexarrays + }; + + Ok (Model { + data, + + header, + text, + meshes, + vertexarrays, + }) + } + + pub fn get_vertex_slice (&self, + vertexarray_index: usize + ) -> &[u8] + { + let vertexarray = &self.vertexarrays [vertexarray_index]; + let bytes_per_float = 4; + let stride = bytes_per_float * vertexarray.va_size; + //assert_eq! (stride, 12); + + let offset: usize = (vertexarray.va_offset).try_into ().unwrap (); + let num_bytes: usize = (stride * self.header.fields [consts::NUM_VERTEXES]).try_into ().unwrap (); + + &self.data [offset..offset + num_bytes] + } + + pub fn get_index_slice (&self, mesh_index: usize) -> &[u8] { + let mesh = &self.meshes [mesh_index]; + let bytes_per_u32 = 4; + let indexes_per_tri = 3; + let stride = bytes_per_u32 * indexes_per_tri; + + let offset: usize = (self.header.fields [consts::OFS_TRIANGLES] + stride * mesh.first_triangle).try_into ().unwrap (); + + let num_bytes: usize = (stride * mesh.num_triangles).try_into ().unwrap (); + + &self.data [offset..offset + num_bytes] + } + + pub fn get_all_indexes (&self) -> &[u8] { + let bytes_per_u32 = 4; + let indexes_per_tri = 3; + let stride = bytes_per_u32 * indexes_per_tri; + + let offset: usize = self.header.fields [consts::OFS_TRIANGLES].try_into ().unwrap (); + + let num_bytes: usize = (stride * self.header.fields [consts::NUM_TRIANGLES]).try_into ().unwrap (); + + &self.data [offset..offset + num_bytes] + } + + // I don't think IQM makes any guarantees about UTF-8 + // so I will only say that the slice has no NULs + + pub fn get_mesh_name (&self, index: usize) -> &[u8] { + let mesh = &self.meshes [index]; + + let ofs: usize = (self.header.fields [consts::OFS_TEXT] + mesh.name).try_into ().unwrap (); + + // There should be an easy way to do this with CString? + let mut nul_index = None; + for (j, c) in self.data [ofs..].iter ().enumerate () { + if *c == 0 { + nul_index = Some (j); + break; + } + } + let nul_index = nul_index.unwrap (); + + &self.data [ofs..ofs + nul_index] + } +} + +#[cfg (test)] +mod test { + use super::*; + + use std::fs::File; + use std::io::Read; + use std::path::Path; + + #[test] + pub fn nothing () { + + } + + fn load_file > (filename: P) -> Vec { + let mut f = File::open (filename).unwrap (); + let mut data = vec! [0u8; f.metadata ().unwrap ().len ().try_into ().unwrap ()]; + f.read_exact (&mut data).unwrap (); + data + } + + #[test] + pub fn load_models () { + let airplane_bytes = load_file ("airplane.iqm"); + let arrow_bytes = load_file ("arrow.iqm"); + let truk_bytes = load_file ("truk.iqm"); + + let airplane = Model::from_slice (&airplane_bytes).unwrap (); + let arrow = Model::from_slice (&arrow_bytes).unwrap (); + let truk = Model::from_slice (&truk_bytes).unwrap (); + + use consts::*; + + assert_eq! (airplane.header.fields [NUM_VERTEXES], 304); + assert_eq! (airplane.header.fields [NUM_TRIANGLES], 156); + assert_eq! (airplane.meshes.len (), 1); + assert_eq! (airplane.vertexarrays.len (), 4); + + assert_eq! (arrow.header.fields [NUM_VERTEXES], 109); + assert_eq! (arrow.header.fields [NUM_TRIANGLES], 117); + assert_eq! (arrow.meshes.len (), 1); + assert_eq! (arrow.vertexarrays.len (), 4); + + assert_eq! (truk.header.fields [NUM_VERTEXES], 256); + assert_eq! (truk.header.fields [NUM_TRIANGLES], 180); + assert_eq! (truk.meshes.len (), 5); + assert_eq! (truk.vertexarrays.len (), 4); + } +} diff --git a/truk.iqm b/truk.iqm new file mode 100644 index 0000000..b5474a7 Binary files /dev/null and b/truk.iqm differ