Skip to content

Commit

Permalink
Implement rotation arithmetics
Browse files Browse the repository at this point in the history
  • Loading branch information
Neo-Zhixing committed Oct 16, 2022
1 parent 1310480 commit fa06283
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 78 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ ahash = { version = "^0.8", optional = true }

[dev-dependencies]
avow = "0.2.0"
glam = "0.21"
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ mod model;
mod palette;
mod parser;
mod scene;
mod types;

pub use types::Rotation;

pub use dot_vox_data::DotVoxData;

Expand Down
80 changes: 2 additions & 78 deletions src/scene.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Color, Dict};
use crate::{Color, Dict, Rotation};
use nom::{
multi::count,
number::complete::{le_i32, le_u32},
Expand Down Expand Up @@ -212,16 +212,6 @@ impl From<Position> 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
Expand All @@ -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);
}
Expand Down
269 changes: 269 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -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<Rotation> 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!();
}
}
}
}
}
}
}

0 comments on commit fa06283

Please sign in to comment.