use std::collections::HashMap;
use rayon::prelude::*;
use crate::geometry::cotan_edge_weight;
use crate::ids::VertexId;
use crate::linalg::vec3::{Vec3, add, scale, sub};
use crate::linalg::{SparseSystem, conjugate_gradient, regularize_diagonal};
use crate::storage::MeshStorage;
use crate::traversal::VertexRing;
type Mat3 = [[f64; 3]; 3];
#[inline]
fn mat3_transpose(m: Mat3) -> Mat3 {
[
[m[0][0], m[1][0], m[2][0]],
[m[0][1], m[1][1], m[2][1]],
[m[0][2], m[1][2], m[2][2]],
]
}
#[inline]
fn mat3_vec(m: Mat3, v: Vec3) -> Vec3 {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
#[inline]
fn mat3_det(m: Mat3) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
fn mat3_adjoint(m: Mat3) -> Mat3 {
[
[
m[1][1] * m[2][2] - m[1][2] * m[2][1],
m[0][2] * m[2][1] - m[0][1] * m[2][2],
m[0][1] * m[1][2] - m[0][2] * m[1][1],
],
[
m[1][2] * m[2][0] - m[1][0] * m[2][2],
m[0][0] * m[2][2] - m[0][2] * m[2][0],
m[0][2] * m[1][0] - m[0][0] * m[1][2],
],
[
m[1][0] * m[2][1] - m[1][1] * m[2][0],
m[0][1] * m[2][0] - m[0][0] * m[2][1],
m[0][0] * m[1][1] - m[0][1] * m[1][0],
],
]
}
fn mat3_inverse(m: Mat3) -> Option<Mat3> {
let det = mat3_det(m);
if det.abs() < 1e-14 {
return None;
}
let adj = mat3_adjoint(m);
let inv_det = 1.0 / det;
let mut r = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
r[i][j] = adj[i][j] * inv_det;
}
}
Some(r)
}
fn polar_rotation(m: Mat3) -> Mat3 {
let mut r = m;
for _ in 0..20 {
let r_inv = match mat3_inverse(r) {
Some(inv) => inv,
None => break, };
let r_inv_t = mat3_transpose(r_inv);
let mut r_new = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
r_new[i][j] = 0.5 * (r[i][j] + r_inv_t[i][j]);
}
}
let mut diff_sq = 0.0;
for i in 0..3 {
for j in 0..3 {
let d = r_new[i][j] - r[i][j];
diff_sq += d * d;
}
}
r = r_new;
if diff_sq < 1e-18 {
break;
}
}
r
}
#[derive(Clone, Copy, Debug)]
pub struct DeformationConstraint {
pub vertex: VertexId,
pub target_position: Vec3,
}
fn build_vertex_index(mesh: &MeshStorage) -> HashMap<VertexId, usize> {
mesh.vertex_ids().enumerate().map(|(i, v)| (v, i)).collect()
}
fn build_neighbors_and_weights(
mesh: &MeshStorage,
v_idx: &HashMap<VertexId, usize>,
) -> Vec<Vec<(usize, f64)>> {
let n = v_idx.len();
let mut neighbors = vec![Vec::new(); n];
for (v, &i) in v_idx {
for he in VertexRing::new(mesh, *v) {
let Some(h) = mesh.get_halfedge(he) else {
continue;
};
let n_vid = h.vertex;
let Some(&j) = v_idx.get(&n_vid) else {
continue;
};
let w = cotan_edge_weight(mesh, he).unwrap_or(0.0) / 2.0;
if w.is_finite() && w.abs() > 1e-12 {
neighbors[i].push((j, w));
}
}
}
neighbors
}
pub fn laplacian_deformation(
mesh: &MeshStorage,
constraints: &[DeformationConstraint],
) -> Option<Vec<Vec3>> {
let n = mesh.vertex_count();
if n == 0 {
return None;
}
let v_idx = build_vertex_index(mesh);
let original: Vec<Vec3> = mesh
.vertex_ids()
.map(|v| mesh.get_vertex(v).map(|x| x.position).unwrap_or([0.0; 3]))
.collect();
let neighbors = build_neighbors_and_weights(mesh, &v_idx);
let delta: Vec<[f64; 3]> = (0..n)
.into_par_iter()
.map(|i| {
let p_i = original[i];
let mut sum = [0.0; 3];
for &(j, w) in &neighbors[i] {
let p_j = original[j];
sum = add(sum, scale(p_j, w));
}
let w_sum: f64 = neighbors[i].iter().map(|&(_, w)| w).sum();
sub(scale(p_i, w_sum), sum)
})
.collect();
let mut constraint_map: HashMap<usize, Vec3> = HashMap::new();
for c in constraints {
if let Some(&idx) = v_idx.get(&c.vertex) {
constraint_map.insert(idx, c.target_position);
}
}
if constraint_map.is_empty() {
return Some(original.clone());
}
let mut sys = SparseSystem::new(n);
let mut rhs_x = vec![0.0; n];
let mut rhs_y = vec![0.0; n];
let mut rhs_z = vec![0.0; n];
for i in 0..n {
if let Some(&target) = constraint_map.get(&i) {
sys.add_diag(i, 1.0);
rhs_x[i] = target[0];
rhs_y[i] = target[1];
rhs_z[i] = target[2];
} else {
let w_sum: f64 = neighbors[i].iter().map(|&(_, w)| w).sum::<f64>() * 2.0;
for &(j, w) in &neighbors[i] {
if constraint_map.contains_key(&j) {
let c = constraint_map[&j];
rhs_x[i] += 2.0 * w * c[0];
rhs_y[i] += 2.0 * w * c[1];
rhs_z[i] += 2.0 * w * c[2];
} else {
sys.add(i, j, -w);
}
}
sys.add_diag(i, w_sum.max(1e-10));
rhs_x[i] += delta[i][0];
rhs_y[i] += delta[i][1];
rhs_z[i] += delta[i][2];
}
}
let mut a = sys.finish();
regularize_diagonal(&mut a, 1e-8);
let x = conjugate_gradient(&a, &rhs_x, n * 200, 1e-6)?;
let y = conjugate_gradient(&a, &rhs_y, n * 200, 1e-6)?;
let z = conjugate_gradient(&a, &rhs_z, n * 200, 1e-6)?;
Some(
x.into_iter()
.zip(y)
.zip(z)
.map(|((x, y), z)| [x, y, z])
.collect(),
)
}
pub fn arap_deformation(
mesh: &MeshStorage,
constraints: &[DeformationConstraint],
iterations: usize,
) -> Option<Vec<Vec3>> {
let n = mesh.vertex_count();
if n == 0 || constraints.is_empty() {
return None;
}
let v_idx = build_vertex_index(mesh);
let original: Vec<Vec3> = mesh
.vertex_ids()
.map(|v| mesh.get_vertex(v).map(|x| x.position).unwrap_or([0.0; 3]))
.collect();
let neighbors = build_neighbors_and_weights(mesh, &v_idx);
let mut constraint_map: HashMap<usize, Vec3> = HashMap::new();
for c in constraints {
if let Some(&idx) = v_idx.get(&c.vertex) {
constraint_map.insert(idx, c.target_position);
}
}
let mut current = laplacian_deformation(mesh, constraints)?;
for (&idx, &target) in &constraint_map {
current[idx] = target;
}
let mut sys = SparseSystem::new(n);
for (i, neighbors_i) in neighbors.iter().enumerate() {
if constraint_map.contains_key(&i) {
sys.add_diag(i, 1.0);
} else {
let w_sum: f64 = neighbors_i.iter().map(|&(_, w)| w).sum::<f64>() * 2.0;
for &(j, w) in neighbors_i {
if !constraint_map.contains_key(&j) {
sys.add(i, j, -w);
}
}
sys.add_diag(i, w_sum.max(1e-10));
}
}
let mut a = sys.finish();
regularize_diagonal(&mut a, 1e-8);
for _ in 0..iterations.max(1) {
let rotations = compute_cell_rotations(&original, ¤t, &neighbors, &constraint_map);
let (rhs_x, rhs_y, rhs_z) =
build_arap_rhs(&original, &neighbors, &rotations, &constraint_map);
let x = conjugate_gradient(&a, &rhs_x, n * 200, 1e-6)?;
let y = conjugate_gradient(&a, &rhs_y, n * 200, 1e-6)?;
let z = conjugate_gradient(&a, &rhs_z, n * 200, 1e-6)?;
current = x
.into_iter()
.zip(y)
.zip(z)
.map(|((x, y), z)| [x, y, z])
.collect();
for (&idx, &target) in &constraint_map {
current[idx] = target;
}
}
Some(current)
}
fn compute_cell_rotations(
original: &[Vec3],
current: &[Vec3],
neighbors: &[Vec<(usize, f64)>],
_constraint_map: &HashMap<usize, Vec3>,
) -> Vec<Mat3> {
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let n = original.len();
(0..n)
.into_par_iter()
.map(|i| {
let mut s = [[0.0; 3]; 3];
let p_i = original[i];
let p_i_cur = current[i];
for &(j, w) in &neighbors[i] {
let e_orig = sub(p_i, original[j]);
let e_cur = sub(p_i_cur, current[j]);
for r in 0..3 {
for c in 0..3 {
s[r][c] += w * e_orig[r] * e_cur[c];
}
}
}
let s_norm = (s[0][0].powi(2)
+ s[0][1].powi(2)
+ s[0][2].powi(2)
+ s[1][0].powi(2)
+ s[1][1].powi(2)
+ s[1][2].powi(2)
+ s[2][0].powi(2)
+ s[2][1].powi(2)
+ s[2][2].powi(2))
.sqrt();
if s_norm < 1e-14 {
identity
} else {
let r = polar_rotation(s);
let det = mat3_det(r);
if det < 0.0 {
let mut r_corrected = r;
r_corrected[2][0] = -r_corrected[2][0];
r_corrected[2][1] = -r_corrected[2][1];
r_corrected[2][2] = -r_corrected[2][2];
r_corrected
} else {
r
}
}
})
.collect()
}
fn build_arap_rhs(
original: &[Vec3],
neighbors: &[Vec<(usize, f64)>],
rotations: &[Mat3],
constraint_map: &HashMap<usize, Vec3>,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let n = neighbors.len();
let rhs: Vec<[f64; 3]> = (0..n)
.into_par_iter()
.map(|i| {
if let Some(&target) = constraint_map.get(&i) {
return target;
}
let r_i = rotations[i];
let p_i = original[i];
let mut acc = [0.0; 3];
for &(j, w) in &neighbors[i] {
let r_j = rotations[j];
let p_j = original[j];
let e = sub(p_i, p_j);
let r_i_e = mat3_vec(r_i, e);
let r_j_e = mat3_vec(r_j, e);
let combined = add(r_i_e, r_j_e);
acc = add(acc, scale(combined, w));
if let Some(&c) = constraint_map.get(&j) {
acc = add(acc, scale(c, 2.0 * w));
}
}
acc
})
.collect();
let rhs_x: Vec<f64> = rhs.iter().map(|r| r[0]).collect();
let rhs_y: Vec<f64> = rhs.iter().map(|r| r[1]).collect();
let rhs_z: Vec<f64> = rhs.iter().map(|r| r[2]).collect();
(rhs_x, rhs_y, rhs_z)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::Vertex;
use crate::test_util::build_icosphere;
use crate::topology_ops::add_triangle;
fn build_grid_mesh() -> MeshStorage {
let mut mesh = MeshStorage::new();
let mut vs: Vec<VertexId> = Vec::new();
for y in 0..3 {
for x in 0..3 {
let v = mesh.add_vertex(Vertex::new([x as f64, y as f64, 0.0]));
vs.push(v);
}
}
for y in 0..2 {
for x in 0..2 {
let v0 = vs[y * 3 + x];
let v1 = vs[y * 3 + x + 1];
let v2 = vs[(y + 1) * 3 + x];
let v3 = vs[(y + 1) * 3 + x + 1];
add_triangle(&mut mesh, v0, v1, v2).unwrap();
add_triangle(&mut mesh, v1, v3, v2).unwrap();
}
}
mesh
}
#[test]
fn test_laplacian_deformation_no_constraint_returns_original() {
let mesh = build_grid_mesh();
let result = laplacian_deformation(&mesh, &[]);
assert!(result.is_some());
let deformed = result.unwrap();
let original: Vec<Vec3> = mesh
.vertex_ids()
.map(|v| mesh.get_vertex(v).unwrap().position)
.collect();
for i in 0..deformed.len() {
for d in 0..3 {
assert!(
(deformed[i][d] - original[i][d]).abs() < 1e-3,
"vertex {} axis {}: deformed {} vs original {}",
i,
d,
deformed[i][d],
original[i][d]
);
}
}
}
#[test]
fn test_laplacian_deformation_single_handle() {
let mesh = build_grid_mesh();
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let constraints = vec![DeformationConstraint {
vertex: vertices[0],
target_position: [0.0, 0.0, 1.0],
}];
let result = laplacian_deformation(&mesh, &constraints);
assert!(result.is_some());
let deformed = result.unwrap();
assert!(
(deformed[0][2] - 1.0).abs() < 1e-3,
"handle z = {}",
deformed[0][2]
);
let mut has_neighbor_displaced = false;
for d in deformed.iter().skip(1) {
if d[2].abs() > 1e-6 {
has_neighbor_displaced = true;
break;
}
}
assert!(
has_neighbor_displaced,
"some non-handle vertex should be displaced"
);
}
#[test]
fn test_laplacian_deformation_two_handles() {
let mesh = build_grid_mesh();
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let constraints = vec![
DeformationConstraint {
vertex: vertices[0],
target_position: [0.0, 0.0, 0.0],
},
DeformationConstraint {
vertex: vertices[8],
target_position: [2.0, 2.0, 1.0],
},
];
let result = laplacian_deformation(&mesh, &constraints);
assert!(result.is_some());
let deformed = result.unwrap();
assert!((deformed[0][2] - 0.0).abs() < 1e-3);
assert!((deformed[8][2] - 1.0).abs() < 1e-3);
}
#[test]
fn test_arap_deformation_basic() {
let mesh = build_grid_mesh();
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let constraints = vec![
DeformationConstraint {
vertex: vertices[0],
target_position: [0.0, 0.0, 0.0],
},
DeformationConstraint {
vertex: vertices[2],
target_position: [2.0, 0.0, 0.0],
},
DeformationConstraint {
vertex: vertices[6],
target_position: [0.0, 2.0, 0.0],
},
DeformationConstraint {
vertex: vertices[8],
target_position: [2.0, 2.0, 1.0],
},
];
let result = arap_deformation(&mesh, &constraints, 5);
assert!(result.is_some());
let deformed = result.unwrap();
assert!((deformed[0][2] - 0.0).abs() < 1e-3);
assert!((deformed[2][2] - 0.0).abs() < 1e-3);
assert!((deformed[6][2] - 0.0).abs() < 1e-3);
assert!((deformed[8][2] - 1.0).abs() < 1e-3);
assert!(
deformed[4][2] > 0.0,
"center vertex z = {} should be > 0",
deformed[4][2]
);
}
#[test]
fn test_arap_deformation_empty_returns_none() {
let mesh = MeshStorage::new();
let result = arap_deformation(&mesh, &[], 5);
assert!(result.is_none());
}
#[test]
fn test_arap_deformation_no_constraints_returns_none() {
let mesh = build_grid_mesh();
let result = arap_deformation(&mesh, &[], 5);
assert!(result.is_none());
}
#[test]
fn test_polar_rotation_identity() {
let m = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let r = polar_rotation(m);
for (i, row) in r.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((val - expected).abs() < 1e-10);
}
}
}
#[test]
fn test_polar_rotation_90deg_around_z() {
let angle = 90.0_f64.to_radians();
let cos90 = angle.cos();
let sin90 = angle.sin();
let m = [[cos90, -sin90, 0.0], [sin90, cos90, 0.0], [0.0, 0.0, 1.0]];
let r = polar_rotation(m);
for i in 0..3 {
for j in 0..3 {
assert!(
(r[i][j] - m[i][j]).abs() < 1e-8,
"r[{}][{}]={} expected {}",
i,
j,
r[i][j],
m[i][j]
);
}
}
}
#[test]
fn test_polar_rotation_stretch_matrix() {
let m = [[2.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let r = polar_rotation(m);
let expected = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
for i in 0..3 {
for j in 0..3 {
assert!((r[i][j] - expected[i][j]).abs() < 1e-8);
}
}
}
#[test]
fn test_laplacian_deformation_icosphere() {
let mesh = build_icosphere(1);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let original_pos = mesh.get_vertex(vertices[0]).unwrap().position;
let target = [
original_pos[0] * 1.5,
original_pos[1] * 1.5,
original_pos[2] * 1.5,
];
let constraints = vec![DeformationConstraint {
vertex: vertices[0],
target_position: target,
}];
let result = laplacian_deformation(&mesh, &constraints);
assert!(result.is_some());
let deformed = result.unwrap();
for d in 0..3 {
assert!((deformed[0][d] - target[d]).abs() < 1e-3);
}
}
}