use approx::assert_relative_eq;
use nalgebra as na;
use std::f32::consts::{FRAC_PI_2, PI};
use oxigaf_flame::{rodrigues, FlameParams, Mesh};
mod rodrigues_tests {
use super::*;
#[test]
fn test_rodrigues_near_180_stability() {
let near_pi_values = [
PI - 1e-4,
PI - 1e-5,
PI - 1e-6,
PI,
PI + 1e-6,
PI + 1e-5,
PI + 1e-4,
];
for &angle in &near_pi_values {
let r_x = rodrigues(angle, 0.0, 0.0);
verify_rotation_matrix(&r_x, &format!("X-axis at angle {angle}"));
let r_y = rodrigues(0.0, angle, 0.0);
verify_rotation_matrix(&r_y, &format!("Y-axis at angle {angle}"));
let r_z = rodrigues(0.0, 0.0, angle);
verify_rotation_matrix(&r_z, &format!("Z-axis at angle {angle}"));
}
}
#[test]
fn test_rodrigues_180_rotation_correctness() {
let r_x = rodrigues(PI, 0.0, 0.0);
let v_y = na::Vector3::new(0.0, 1.0, 0.0);
let rotated = r_x * v_y;
assert_relative_eq!(rotated.x, 0.0, epsilon = 1e-5);
assert_relative_eq!(rotated.y, -1.0, epsilon = 1e-5);
assert_relative_eq!(rotated.z, 0.0, epsilon = 1e-5);
let r_y = rodrigues(0.0, PI, 0.0);
let v_x = na::Vector3::new(1.0, 0.0, 0.0);
let rotated = r_y * v_x;
assert_relative_eq!(rotated.x, -1.0, epsilon = 1e-5);
assert_relative_eq!(rotated.y, 0.0, epsilon = 1e-5);
assert_relative_eq!(rotated.z, 0.0, epsilon = 1e-5);
let r_z = rodrigues(0.0, 0.0, PI);
let rotated = r_z * v_x;
assert_relative_eq!(rotated.x, -1.0, epsilon = 1e-5);
assert_relative_eq!(rotated.y, 0.0, epsilon = 1e-5);
assert_relative_eq!(rotated.z, 0.0, epsilon = 1e-5);
}
#[test]
fn test_rodrigues_360_is_identity() {
let r_360 = rodrigues(2.0 * PI, 0.0, 0.0);
let identity = na::Matrix3::<f32>::identity();
assert_matrix_near(&r_360, &identity, 1e-4);
}
#[test]
fn test_rodrigues_small_angles() {
let small_angles = [1e-7, 1e-8, 1e-9, 1e-10];
for &angle in &small_angles {
let r = rodrigues(angle, angle, angle);
let identity = na::Matrix3::<f32>::identity();
assert_matrix_near(&r, &identity, 1e-5);
}
}
#[test]
fn test_rodrigues_negative_angles() {
let angle = 0.5;
let r_pos = rodrigues(angle, 0.0, 0.0);
let r_neg = rodrigues(-angle, 0.0, 0.0);
assert_matrix_near(&r_neg, &r_pos.transpose(), 1e-6);
}
#[test]
fn test_rodrigues_combined_rotations() {
let r_90 = rodrigues(0.0, 0.0, FRAC_PI_2);
let r_180 = rodrigues(0.0, 0.0, PI);
let combined = r_90 * r_90;
assert_matrix_near(&combined, &r_180, 1e-5);
}
#[test]
fn test_rodrigues_preserves_rotation_axis() {
let norm = (0.3f32 * 0.3 + 0.4 * 0.4 + 0.5 * 0.5).sqrt();
let axis = na::Vector3::new(0.3 / norm, 0.4 / norm, 0.5 / norm);
let angle = 1.2;
let r = rodrigues(axis.x * angle, axis.y * angle, axis.z * angle);
let rotated_axis = r * axis;
assert_relative_eq!(rotated_axis.x, axis.x, epsilon = 1e-5);
assert_relative_eq!(rotated_axis.y, axis.y, epsilon = 1e-5);
assert_relative_eq!(rotated_axis.z, axis.z, epsilon = 1e-5);
}
fn verify_rotation_matrix(r: &na::Matrix3<f32>, context: &str) {
let product = r.transpose() * r;
let identity = na::Matrix3::<f32>::identity();
for i in 0..3 {
for j in 0..3 {
assert!(
(product[(i, j)] - identity[(i, j)]).abs() < 1e-4,
"Orthogonality failed for {context}: product[{i},{j}] = {} (expected {})",
product[(i, j)],
identity[(i, j)]
);
}
}
let det = r.determinant();
assert!(
(det - 1.0).abs() < 1e-4,
"Determinant failed for {context}: det = {det} (expected 1.0)"
);
}
fn assert_matrix_near(a: &na::Matrix3<f32>, b: &na::Matrix3<f32>, epsilon: f32) {
for i in 0..3 {
for j in 0..3 {
assert!(
(a[(i, j)] - b[(i, j)]).abs() < epsilon,
"Matrix mismatch at [{i},{j}]: {} vs {} (epsilon = {epsilon})",
a[(i, j)],
b[(i, j)]
);
}
}
}
}
mod mesh_normal_tests {
use super::*;
#[test]
fn test_mesh_normals_xy_plane() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let mesh = Mesh::new(vertices, vec![[0, 1, 2]]);
for normal in &mesh.normals {
assert_relative_eq!(normal.x, 0.0, epsilon = 1e-5);
assert_relative_eq!(normal.y, 0.0, epsilon = 1e-5);
assert_relative_eq!(normal.z, 1.0, epsilon = 1e-3);
}
}
#[test]
fn test_mesh_normals_xz_plane() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(0.0, 0.0, 1.0),
na::Point3::new(1.0, 0.0, 0.0),
];
let mesh = Mesh::new(vertices, vec![[0, 1, 2]]);
for normal in &mesh.normals {
assert_relative_eq!(normal.x, 0.0, epsilon = 1e-5);
assert_relative_eq!(normal.y, 1.0, epsilon = 1e-3);
assert_relative_eq!(normal.z, 0.0, epsilon = 1e-5);
}
}
#[test]
fn test_mesh_normals_unit_length() {
let vertices = vec![
na::Point3::new(1.0, 0.0, -1.0 / 2.0f32.sqrt()),
na::Point3::new(-1.0, 0.0, -1.0 / 2.0f32.sqrt()),
na::Point3::new(0.0, 1.0, 1.0 / 2.0f32.sqrt()),
na::Point3::new(0.0, -1.0, 1.0 / 2.0f32.sqrt()),
];
let faces = vec![[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]];
let mesh = Mesh::new(vertices, faces);
for normal in &mesh.normals {
let len = normal.norm();
assert_relative_eq!(len, 1.0, epsilon = 1e-5);
}
}
#[test]
fn test_mesh_normals_area_weighted() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(0.1, 0.0, 0.0),
na::Point3::new(0.0, 0.1, 0.0),
na::Point3::new(10.0, 0.0, 0.0),
na::Point3::new(0.0, 10.0, 0.0),
];
let faces = vec![
[0, 1, 2], [0, 3, 4], ];
let mesh = Mesh::new(vertices, faces);
for normal in &mesh.normals {
assert_relative_eq!(normal.x, 0.0, epsilon = 1e-5);
assert_relative_eq!(normal.y, 0.0, epsilon = 1e-5);
assert_relative_eq!(normal.z, 1.0, epsilon = 1e-3);
}
}
#[test]
fn test_mesh_normals_quad() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(1.0, 1.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let faces = vec![[0, 1, 2], [0, 2, 3]];
let mesh = Mesh::new(vertices, faces);
for normal in &mesh.normals {
assert_relative_eq!(normal.z, 1.0, epsilon = 1e-3);
}
}
#[test]
fn test_mesh_normals_recompute() {
let mut vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let mut mesh = Mesh::new(vertices.clone(), vec![[0, 1, 2]]);
assert_relative_eq!(mesh.normals[0].z, 1.0, epsilon = 1e-3);
vertices[1] = na::Point3::new(0.0, 1.0, 0.0);
vertices[2] = na::Point3::new(1.0, 0.0, 0.0);
mesh = Mesh::new(vertices, vec![[0, 1, 2]]);
assert_relative_eq!(mesh.normals[0].z, -1.0, epsilon = 1e-3);
}
#[test]
fn test_mesh_normals_degenerate_triangle() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(2.0, 0.0, 0.0),
];
let mesh = Mesh::new(vertices, vec![[0, 1, 2]]);
for normal in &mesh.normals {
let len = normal.norm();
assert!(len < 1e-5 || (len - 1.0).abs() < 1e-5);
}
}
}
mod blend_shapes_tests {
use super::*;
#[test]
fn test_zero_coefficients_no_deformation() {
let params_zero = FlameParams::builder()
.shape(vec![0.0, 0.0, 0.0])
.expression(vec![0.0, 0.0])
.build();
let params_empty = FlameParams::neutral();
assert_eq!(params_zero.shape, vec![0.0, 0.0, 0.0]);
assert_eq!(params_empty.shape.len(), 0);
}
#[test]
fn test_blend_shape_coefficient_storage() {
let shape_coeffs = vec![0.5, -0.3, 0.8, -1.2, 0.1];
let expr_coeffs = vec![0.2, -0.4, 0.6];
let params = FlameParams::builder()
.shape(shape_coeffs.clone())
.expression(expr_coeffs.clone())
.build();
assert_eq!(params.shape, shape_coeffs);
assert_eq!(params.expression, expr_coeffs);
}
#[test]
fn test_blend_shape_params_support_linearity() {
let coeff = 0.5;
let params_half = FlameParams::builder().shape(vec![coeff]).build();
let params_double = FlameParams::builder().shape(vec![coeff * 2.0]).build();
assert_relative_eq!(
params_double.shape[0],
params_half.shape[0] * 2.0,
epsilon = 1e-10
);
}
}
mod lbs_tests {
use super::*;
#[test]
fn test_joint_pose_extraction() {
let params = FlameParams::builder()
.root_rotation([0.1, 0.2, 0.3])
.neck_rotation([0.4, 0.5, 0.6])
.jaw_rotation_full([0.7, 0.8, 0.9])
.left_eye_rotation([1.0, 1.1, 1.2])
.right_eye_rotation([1.3, 1.4, 1.5])
.build();
assert_eq!(params.joint_pose(0), [0.1, 0.2, 0.3]); assert_eq!(params.joint_pose(1), [0.4, 0.5, 0.6]); assert_eq!(params.joint_pose(2), [0.7, 0.8, 0.9]); assert_eq!(params.joint_pose(3), [1.0, 1.1, 1.2]); assert_eq!(params.joint_pose(4), [1.3, 1.4, 1.5]); }
#[test]
fn test_joint_pose_out_of_bounds() {
let params = FlameParams::builder()
.root_rotation([0.1, 0.2, 0.3])
.build();
assert_eq!(params.joint_pose(5), [0.0, 0.0, 0.0]);
assert_eq!(params.joint_pose(10), [0.0, 0.0, 0.0]);
assert_eq!(params.joint_pose(100), [0.0, 0.0, 0.0]);
}
#[test]
fn test_neutral_pose_identity() {
let params = FlameParams::neutral();
for j in 0..5 {
let [rx, ry, rz] = params.joint_pose(j);
let r = rodrigues(rx, ry, rz);
let identity = na::Matrix3::<f32>::identity();
for i in 0..3 {
for k in 0..3 {
assert_relative_eq!(r[(i, k)], identity[(i, k)], epsilon = 1e-6);
}
}
}
}
#[test]
fn test_translation_storage() {
let translation = [1.0, 2.0, 3.0];
let params = FlameParams::builder().translation(translation).build();
assert_eq!(params.translation, translation);
}
#[test]
fn test_skinning_weights_concept() {
let weights = [0.3, 0.3, 0.2, 0.1, 0.1];
let sum: f32 = weights.iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
}
}
mod validation_tests {
use super::*;
#[test]
fn test_validate_within_bounds() {
let params = FlameParams::builder()
.shape(vec![1.0, -1.0, 2.0, -2.0])
.expression(vec![0.5, -0.5, 1.0, -1.0])
.jaw_rotation(0.1)
.translation([0.5, -0.5, 0.0])
.build();
assert!(params.validate());
}
#[test]
fn test_validate_shape_out_of_bounds() {
let params = FlameParams::builder()
.shape(vec![3.5]) .build();
assert!(!params.validate());
}
#[test]
fn test_validate_expression_out_of_bounds() {
let params = FlameParams::builder()
.expression(vec![2.5]) .build();
assert!(!params.validate());
}
#[test]
fn test_validate_pose_out_of_bounds() {
let params = FlameParams::builder()
.root_rotation([4.0, 0.0, 0.0]) .build();
assert!(!params.validate());
}
#[test]
fn test_validate_translation_out_of_bounds() {
let params = FlameParams::builder()
.translation([1.5, 0.0, 0.0]) .build();
assert!(!params.validate());
}
#[test]
fn test_validate_exactly_at_bounds() {
let params = FlameParams::builder()
.shape(vec![3.0, -3.0])
.expression(vec![2.0, -2.0])
.translation([1.0, -1.0, 1.0])
.build();
assert!(params.validate());
}
}
mod edge_case_tests {
use super::*;
#[test]
fn test_empty_mesh() {
let mesh = Mesh::new(vec![], vec![]);
assert_eq!(mesh.num_vertices(), 0);
assert_eq!(mesh.num_faces(), 0);
assert!(mesh.normals.is_empty());
}
#[test]
fn test_single_vertex_mesh() {
let vertices = vec![na::Point3::new(0.0, 0.0, 0.0)];
let mesh = Mesh::new(vertices, vec![]);
assert_eq!(mesh.num_vertices(), 1);
assert_eq!(mesh.num_faces(), 0);
}
#[test]
fn test_params_with_max_pose_values() {
let max_pose = vec![PI; 15];
let params = FlameParams::builder().pose(max_pose).build();
assert_eq!(params.pose.len(), 15);
assert!(params.validate()); }
#[test]
fn test_mesh_face_area() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(2.0, 0.0, 0.0),
na::Point3::new(0.0, 2.0, 0.0),
];
let mesh = Mesh::new(vertices, vec![[0, 1, 2]]);
let area = mesh.face_area(&[0, 1, 2]);
assert_relative_eq!(area, 2.0, epsilon = 1e-5);
}
#[test]
fn test_mesh_face_area_unit_triangle() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let mesh = Mesh::new(vertices, vec![[0, 1, 2]]);
let area = mesh.face_area(&[0, 1, 2]);
assert_relative_eq!(area, 0.5, epsilon = 1e-5);
}
}
mod batched_normal_tests {
use super::*;
use oxigaf_flame::{compute_normals_batch, compute_normals_into};
#[test]
fn test_compute_normals_into_matches_mesh() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let faces = vec![[0, 1, 2]];
let mesh = Mesh::new(vertices.clone(), faces.clone());
let mut normals_out = vec![na::Vector3::zeros(); vertices.len()];
compute_normals_into(&vertices, &faces, &mut normals_out);
for (n1, n2) in mesh.normals.iter().zip(normals_out.iter()) {
assert_relative_eq!(n1.x, n2.x, epsilon = 1e-6);
assert_relative_eq!(n1.y, n2.y, epsilon = 1e-6);
assert_relative_eq!(n1.z, n2.z, epsilon = 1e-6);
}
}
#[test]
fn test_compute_normals_batch_independent() {
let vertices1 = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let vertices2 = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(0.0, 0.0, 1.0),
na::Point3::new(1.0, 0.0, 0.0),
];
let faces = vec![[0, 1, 2]];
let vertices_batch = vec![vertices1.clone(), vertices2.clone()];
let mut normals_batch = vec![vec![na::Vector3::zeros(); 3], vec![na::Vector3::zeros(); 3]];
compute_normals_batch(&vertices_batch, &faces, &mut normals_batch);
for normal in &normals_batch[0] {
assert_relative_eq!(normal.z, 1.0, epsilon = 1e-3);
}
for normal in &normals_batch[1] {
assert_relative_eq!(normal.y, 1.0, epsilon = 1e-3);
}
}
#[test]
fn test_batch_of_one_matches_single() {
let vertices = vec![
na::Point3::new(0.0, 0.0, 0.0),
na::Point3::new(1.0, 0.0, 0.0),
na::Point3::new(0.0, 1.0, 0.0),
];
let faces = vec![[0, 1, 2]];
let mut single_normals = vec![na::Vector3::zeros(); 3];
compute_normals_into(&vertices, &faces, &mut single_normals);
let vertices_batch = vec![vertices];
let mut batch_normals = vec![vec![na::Vector3::zeros(); 3]];
compute_normals_batch(&vertices_batch, &faces, &mut batch_normals);
for (n1, n2) in single_normals.iter().zip(batch_normals[0].iter()) {
assert_relative_eq!(n1.x, n2.x, epsilon = 1e-6);
assert_relative_eq!(n1.y, n2.y, epsilon = 1e-6);
assert_relative_eq!(n1.z, n2.z, epsilon = 1e-6);
}
}
#[test]
fn test_empty_batch() {
let vertices_batch: Vec<Vec<na::Point3<f32>>> = vec![];
let faces: Vec<[u32; 3]> = vec![];
let mut normals_batch: Vec<Vec<na::Vector3<f32>>> = vec![];
compute_normals_batch(&vertices_batch, &faces, &mut normals_batch);
assert!(normals_batch.is_empty());
}
}
mod batched_output_tests {
use super::*;
use oxigaf_flame::BatchedFlameOutput;
#[test]
fn test_batched_output_with_capacity() {
let batch_size = 5;
let num_vertices = 100;
let faces = vec![[0, 1, 2], [1, 2, 3]];
let output = BatchedFlameOutput::with_capacity(batch_size, num_vertices, faces.clone());
assert_eq!(output.batch_size, batch_size);
assert_eq!(output.num_vertices(), num_vertices);
assert_eq!(output.vertices.len(), batch_size);
assert_eq!(output.normals.len(), batch_size);
assert_eq!(output.faces.len(), 2);
for verts in &output.vertices {
assert_eq!(verts.len(), num_vertices);
}
for norms in &output.normals {
assert_eq!(norms.len(), num_vertices);
}
}
#[test]
fn test_get_mesh() {
let batch_size = 3;
let num_vertices = 4;
let faces = vec![[0, 1, 2], [0, 2, 3]];
let mut output = BatchedFlameOutput::with_capacity(batch_size, num_vertices, faces);
output.vertices[1][0] = na::Point3::new(1.0, 2.0, 3.0);
output.normals[1][0] = na::Vector3::new(0.0, 0.0, 1.0);
let mesh = output.get_mesh(1);
assert!(mesh.is_some());
let mesh = mesh.expect("mesh should exist");
assert_eq!(mesh.vertices[0], na::Point3::new(1.0, 2.0, 3.0));
assert_eq!(mesh.normals[0], na::Vector3::new(0.0, 0.0, 1.0));
assert_eq!(mesh.faces.len(), 2);
assert!(output.get_mesh(10).is_none());
}
#[test]
fn test_into_meshes() {
let batch_size = 2;
let num_vertices = 3;
let faces = vec![[0, 1, 2]];
let mut output = BatchedFlameOutput::with_capacity(batch_size, num_vertices, faces);
output.vertices[0][0] = na::Point3::new(1.0, 0.0, 0.0);
output.vertices[1][0] = na::Point3::new(2.0, 0.0, 0.0);
let meshes = output.into_meshes();
assert_eq!(meshes.len(), batch_size);
assert_eq!(meshes[0].vertices[0], na::Point3::new(1.0, 0.0, 0.0));
assert_eq!(meshes[1].vertices[0], na::Point3::new(2.0, 0.0, 0.0));
}
}
mod buffer_pool_tests {
use oxigaf_flame::BatchBufferPool;
#[test]
fn test_buffer_pool_new() {
let batch_size = 4;
let num_vertices = 50;
let n_joints = 5;
let pool = BatchBufferPool::new(batch_size, num_vertices, n_joints);
assert_eq!(pool.capacity(), batch_size);
}
#[test]
fn test_buffer_pool_ensure_capacity() {
let mut pool = BatchBufferPool::new(2, 50, 5);
assert_eq!(pool.capacity(), 2);
pool.ensure_capacity(5);
assert_eq!(pool.capacity(), 5);
pool.ensure_capacity(3);
assert_eq!(pool.capacity(), 5);
}
#[test]
fn test_buffer_pool_clear() {
let mut pool = BatchBufferPool::new(2, 10, 5);
pool.clear();
assert_eq!(pool.capacity(), 2);
}
}