diff --git a/Cargo.toml b/Cargo.toml index c5a6ca1..51d26ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ ahash = { version = "^0.8", optional = true } [dev-dependencies] avow = "0.2.0" +glam = "0.21" diff --git a/src/lib.rs b/src/lib.rs index df156c5..7140286 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,9 @@ mod model; mod palette; mod parser; mod scene; +mod types; + +pub use types::Rotation; pub use dot_vox_data::DotVoxData; diff --git a/src/scene.rs b/src/scene.rs index 38969d5..4ddb086 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -1,4 +1,4 @@ -use crate::{Color, Dict}; +use crate::{Color, Dict, Rotation}; use nom::{ multi::count, number::complete::{le_i32, le_u32}, @@ -212,16 +212,6 @@ impl From for (i32, i32, i32) { } } -/// Represents a rotation. Used to orient a chunk relative to other chunks. -/// The rotation is represented as a row-major 3×3 matrix (this is how it -/// appears in the `.vox` format). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Rotation { - /// This is a row-major representation of the rotation as an orthonormal 3×3 - /// matrix. The entries are in [-1..1]. - rot: [[i8; 3]; 3], -} - #[derive(Clone, Debug, PartialEq, Eq, Default)] /// Represents an animation. The chunk is oriented according to the rotation /// (`_r`) is placed at the position (`t`) specified. The Rotation is @@ -246,73 +236,7 @@ impl Frame { if let IResult::<&str, u8>::Ok((_, byte_rotation)) = nom::character::complete::u8(value.as_str()) { - // .vox stores a row-major rotation in the bits of a byte. - // - // for example : - // R = - // 0 1 0 - // 0 0 -1 - // -1 0 0 - // ==> - // unsigned char _r = (1 << 0) | (2 << 2) | (0 << 4) | (1 << 5) | (1 << 6) - // - // bit | value - // 0-1 : 1 : index of the non-zero entry in the first row - // 2-3 : 2 : index of the non-zero entry in the second row - // 4 : 0 : the sign in the first row (0 : positive; 1 : negative) - // 5 : 1 : the sign in the second row (0 : positive; 1 : negative) - // 6 : 1 : the sign in the third row (0 : positive; 1 : negative) - - // First two indices - let index_nz1 = (byte_rotation & 0b11) as usize; - let index_nz2 = ((byte_rotation & 0b1100) >> 2) as usize; - - if index_nz1 == index_nz2 { - debug!("'_r' in Frame is not orthnonormal! {}", value); - return None; - } - - // You get the third index out via a process of elimination here. It's the one - // that wasn't used for the other rows. - let possible_thirds = [ - index_nz1 == 0 || index_nz2 == 0, - index_nz1 == 1 || index_nz2 == 1, - index_nz1 == 2 || index_nz2 == 2, - ]; - - let mut index_nz3 = 0; - - for (i, possible_third) in possible_thirds.iter().enumerate() { - if !possible_third { - index_nz3 = i; - } - } - - // Values of all three columns (1 or 0) - let val_1 = if (byte_rotation & 0b1_0000) >> 4 == 1 { - -1 - } else { - 1 - }; - let val_2 = if (byte_rotation & 0b10_0000) >> 5 == 1 { - -1 - } else { - 1 - }; - let val_3 = if (byte_rotation & 0b100_0000) >> 6 == 1 { - -1 - } else { - 1 - }; - - // Rows as read from file - let mut initial_rows: [[i8; 3]; 3] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; - - initial_rows[0][index_nz1] = val_1; - initial_rows[1][index_nz2] = val_2; - initial_rows[2][index_nz3] = val_3; - - return Some(Rotation { rot: initial_rows }); + return Some(Rotation::from_byte(byte_rotation)) } else { debug!("'_r' attribute for Frame could not be parsed! {}", value); } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..a7368b9 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,269 @@ +/// A **[`Signed Permutation Matrix`]** [^note] encoded in a byte. +/// +/// # Encoding +/// The encoding follows the MagicaVoxel [ROTATION] type. +/// +/// for example : +/// ``` +/// let R = [ +/// [0, 1, 0], +/// [0, 0, -1], +/// [-1, 0, 0], +/// ]; +/// let _r: u8 = (1 << 0) | (2 << 2) | (0 << 4) | (1 << 5) | (1 << 6); +/// ``` +/// +/// | bit | value | descripton | +/// |-----|-------|---------------------------------------------------------| +/// | 0-1 | 1 | Index of the non-zero entry in the first row | +/// | 2-3 | 2 | Index of the non-zero entry in the second row | +/// | 4 | 0 | The sign in the first row (0 - positive; 1 - negative)| +/// | 5 | 1 | The sign in the second row (0 - positive; 1 - negative)| +/// | 6 | 1 | The sign in the third row (0 - positive; 1 - negative) | +/// +/// [`Signed Permutation Matrix`]: https://en.wikipedia.org/wiki/Generalized_permutation_matrix#Signed_permutation_group +/// [ROTATION]: https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox-extension.txt#L24 +/// [^note]: A [`Signed Permutation Matrix`] is a square binary matrix that has exactly one entry of ±1 in each row and each column and 0s elsewhere. +#[derive(Clone, Copy)] +pub struct Rotation(u8); + +pub type Quat = [f32; 4]; +pub type Vec3 = [f32; 3]; + +impl Rotation { + pub const IDENTITY: Self = Rotation(0b0000100); + + pub fn from_byte(byte: u8) -> Self { + let index_nz1 = byte & 0b11; + let index_nz2 = (byte >> 2) & 0b11; + assert!( + (index_nz1 != index_nz2) && (index_nz1 != 0b11 && index_nz2 != 0b11), + "Invalid Rotation" + ); + Rotation(byte) + } + + /// Decompose the Signed Permutation Matrix into a rotation component, represented by a Quaternion, + /// and a flip component, represented by a Vec3 which is either Vec3::ONE or -Vec3::ONE. + pub fn to_quat_scale(&self) -> (Quat, Vec3) { + let index_nz1 = self.0 & 0b11; + let index_nz2 = (self.0 >> 2) & 0b11; + let flip = (self.0 >> 4) as usize; + + let si = [1.0, 1.0, 1.0]; // scale_identity + let sf = [-1.0, -1.0, -1.0]; // scale_flip + const SQRT_2_2: f32 = std::f32::consts::SQRT_2 / 2.0; + match (index_nz1, index_nz2) { + (0, 1) => { + let quats = [ + [0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + ]; + let mapping = [0, 3, 2, 1, 1, 2, 3, 0]; + let scales = [si, sf, sf, si, sf, si, si, sf]; + (quats[mapping[flip]], scales[flip]) + } + (0, 2) => { + let quats = [ + [0.0, SQRT_2_2, SQRT_2_2, 0.0], + [SQRT_2_2, 0.0, 0.0, SQRT_2_2], + [SQRT_2_2, 0.0, 0.0, -SQRT_2_2], + [0.0, SQRT_2_2, -SQRT_2_2, 0.0], + ]; + let mapping = [3, 0, 1, 2, 2, 1, 0, 3]; + let scales = [sf, si, si, sf, si, sf, sf, si]; + (quats[mapping[flip]], scales[flip]) + } + (1, 2) => { + let quats = [ + [0.5, 0.5, 0.5, -0.5], + [0.5, -0.5, 0.5, 0.5], + [0.5, -0.5, -0.5, -0.5], + [0.5, 0.5, -0.5, 0.5], + ]; + let mapping = [0, 3, 2, 1, 1, 2, 3, 0]; + let scales = [si, sf, sf, si, sf, si, si, sf]; + (quats[mapping[flip]], scales[flip]) + } + (1, 0) => { + let quats = [ + [0.0, 0.0, SQRT_2_2, SQRT_2_2], + [0.0, 0.0, SQRT_2_2, -SQRT_2_2], + [SQRT_2_2, SQRT_2_2, 0.0, 0.0], + [SQRT_2_2, -SQRT_2_2, 0.0, 0.0], + ]; + let mapping = [3, 0, 1, 2, 2, 1, 0, 3]; + let scales = [sf, si, si, sf, si, sf, sf, si]; + + (quats[mapping[flip]], scales[flip]) + } + (2, 0) => { + let quats = [ + [0.5, 0.5, 0.5, 0.5], + [0.5, -0.5, -0.5, 0.5], + [0.5, 0.5, -0.5, -0.5], + [0.5, -0.5, 0.5, -0.5], + ]; + let mapping = [0, 3, 2, 1, 1, 2, 3, 0]; + let scales = [si, sf, sf, si, sf, si, si, sf]; + (quats[mapping[flip]], scales[flip]) + } + (2, 1) => { + let quats = [ + [0.0, SQRT_2_2, 0.0, -SQRT_2_2], + [SQRT_2_2, 0.0, SQRT_2_2, 0.0], + [0.0, SQRT_2_2, 0.0, SQRT_2_2], + [SQRT_2_2, 0.0, -SQRT_2_2, 0.0], + ]; + let mapping = [3, 0, 1, 2, 2, 1, 0, 3]; + let scales = [sf, si, si, sf, si, sf, sf, si]; + (quats[mapping[flip]], scales[flip]) + } + _ => unreachable!(), + } + } + + pub fn to_cols_array_2d(&self) -> [[f32; 3]; 3] { + let mut cols: [[f32; 3]; 3] = [[0.0; 3]; 3]; + + let index_nz1 = self.0 & 0b11; + let index_nz2 = (self.0 >> 2) & 0b11; + let index_nz3 = 3 - index_nz1 - index_nz2; + + let row_1_sign: f32 = if self.0 & (1 << 4) == 0 { 1.0 } else { -1.0 }; + let row_2_sign: f32 = if self.0 & (1 << 5) == 0 { 1.0 } else { -1.0 }; + let row_3_sign: f32 = if self.0 & (1 << 6) == 0 { 1.0 } else { -1.0 }; + + cols[index_nz1 as usize][0] = row_1_sign; + cols[index_nz2 as usize][1] = row_2_sign; + cols[index_nz3 as usize][2] = row_3_sign; + + cols + } +} + +impl std::fmt::Debug for Rotation { + /// Print the Rotation in a format that looks like `Rotation(-y, -z, x)` + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + let index_nz1 = self.0 & 0b11; + let index_nz2 = (self.0 >> 2) & 0b11; + let index_nz3 = 3 - index_nz1 - index_nz2; + + let xyz: &[char; 3] = &['x', 'y', 'z']; + + f.write_str("Rotation(")?; + + if self.0 & (1 << 4) != 0 { + f.write_char('-')?; + }; + f.write_char(xyz[index_nz1 as usize])?; + f.write_char(' ')?; + + if self.0 & (1 << 5) != 0 { + f.write_char('-')?; + }; + f.write_char(xyz[index_nz2 as usize])?; + f.write_char(' ')?; + + if self.0 & (1 << 6) != 0 { + f.write_char('-')?; + }; + f.write_char(xyz[index_nz3 as usize])?; + f.write_char(')')?; + Ok(()) + } +} + +impl std::ops::Mul for Rotation { + type Output = Rotation; + + /// Integer-only multiplication of two Rotation. + fn mul(self, rhs: Rotation) -> Rotation { + let mut rhs_rows = [rhs.0 & 0b11, (rhs.0 >> 2) & 0b11, 0]; + rhs_rows[2] = 3 - rhs_rows[0] - rhs_rows[1]; + + let mut lhs_rows = [self.0 & 0b11, (self.0 >> 2) & 0b11, 0]; + lhs_rows[2] = 3 - lhs_rows[0] - lhs_rows[1]; + let lhs_signs = self.0 >> 4; + + let result_row_0 = rhs_rows[lhs_rows[0] as usize]; + let result_row_1 = rhs_rows[lhs_rows[1] as usize]; + let rhs_signs = rhs.0 >> 4; + + let rhs_signs_0 = (rhs_signs >> lhs_rows[0]) & 1; + let rhs_signs_1 = (rhs_signs >> lhs_rows[1]) & 1; + let rhs_signs_2 = (rhs_signs >> lhs_rows[2]) & 1; + let rhs_signs_permutated: u8 = rhs_signs_0 | (rhs_signs_1 << 1) | (rhs_signs_2 << 2); + let signs = lhs_signs ^ rhs_signs_permutated; + Rotation(result_row_0 | (result_row_1 << 2) | (signs << 4)) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_spm_mul() { + use super::Rotation as SPM; + let spms: [u8; 6] = [0b0001, 0b0010, 0b0100, 0b0110, 0b1000, 0b1001]; + + // Test for every possible spms + for i in 0..6 { + for j in 0..6 { + for sign_i in 0..8 { + for sign_j in 0..8 { + let spm_lhs = SPM(spms[i] | (sign_i << 4)); + let spm_rhs = SPM(spms[j] | (sign_j << 4)); + let spm_mul = spm_lhs * spm_rhs; + let spm_mul_mat: glam::Mat3 = + glam::Mat3::from_cols_array_2d(&spm_mul.to_cols_array_2d()); + + let spm_lhs_mat: glam::Mat3 = + glam::Mat3::from_cols_array_2d(&spm_lhs.to_cols_array_2d()); + let spm_rhs_mat: glam::Mat3 = + glam::Mat3::from_cols_array_2d(&spm_rhs.to_cols_array_2d()); + + // Compare the result of the integer-only multiplication with the + // result of the f32 matrix multiplication + let spm_reference_result: glam::Mat3 = spm_lhs_mat * spm_rhs_mat; + assert_eq!(spm_mul_mat, spm_reference_result); + } + } + } + } + } + + #[test] + fn test_to_quat_scale() { + use super::Rotation as SPM; + let spms: [u8; 6] = [0b0100, 0b1000, 0b1001, 0b0001, 0b0010, 0b0110]; + + // Test for every possible spms + for i in 0..6 { + for sign_i in 0..8 { + let spm = SPM(spms[i] | (sign_i << 4)); + let (rotation, scale) = spm.to_quat_scale(); + let rotation = glam::Quat::from_array(rotation); + let scale: glam::Vec3 = scale.into(); + let rs_mat = glam::Mat3::from_quat(rotation) + * glam::mat3( + glam::Vec3::new(scale.x, 0.0, 0.0), + glam::Vec3::new(0.0, scale.y, 0.0), + glam::Vec3::new(0.0, 0.0, scale.z), + ); + let reference_mat: glam::Mat3 = + glam::Mat3::from_cols_array_2d(&spm.to_cols_array_2d()); + for i in 0..3 { + for j in 0..3 { + if (reference_mat.col(i)[j] - rs_mat.col(i)[j]).abs() > 0.00001 { + println!("{:?} vs {:?}", rs_mat, reference_mat); + panic!(); + } + } + } + } + } + } +} \ No newline at end of file