use std::num::NonZeroU8;
use subdiv_kernels::{
Refinement, RefinementResult, Refiner, Scheme, SchemeOptions, Adjacency,
Mesh, UniformRefine, VertexOrigin,
};
const SHALLOW_LEVEL: u8 = 2;
const DEEP_LEVEL: u8 = SHALLOW_LEVEL + 8;
const POSITION_TOLERANCE: f64 = 1e-3;
const NORMAL_TOLERANCE_DEG: f64 = 1.0;
fn v3(p: [f32; 3]) -> [f64; 3] {
[p[0] as f64, p[1] as f64, p[2] as f64]
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn length(a: [f64; 3]) -> f64 {
dot(a, a).sqrt()
}
fn normalize(a: [f64; 3]) -> [f64; 3] {
let len = length(a);
assert!(len > 1e-12, "degenerate vector cannot be normalized: {a:?}");
[a[0] / len, a[1] / len, a[2] / len]
}
fn angle_deg(a: [f64; 3], b: [f64; 3]) -> f64 {
dot(normalize(a), normalize(b)).clamp(-1.0, 1.0).acos() * 180.0 / std::f64::consts::PI
}
fn quad_grid(n: u32, height: impl Fn(u32, u32) -> f32) -> (Mesh, Vec<[f32; 3]>) {
let stride = n + 1;
let height = &height;
let positions: Vec<[f32; 3]> = (0..stride)
.flat_map(|i| (0..stride).map(move |j| [i as f32, height(i, j), j as f32]))
.collect();
let vid = |i: u32, j: u32| i * stride + j;
let face_vertex_indices: Vec<u32> = (0..n)
.flat_map(|i| {
(0..n).flat_map(move |j| {
[vid(i, j), vid(i + 1, j), vid(i + 1, j + 1), vid(i, j + 1)]
})
})
.collect();
let topo = Mesh {
vertex_count: stride * stride,
face_vertex_counts: vec![4; (n * n) as usize],
face_vertex_indices,
edge_vertices: Vec::new(),
edge_creases: Vec::new(),
vertex_corners: vec![0.0; (stride * stride) as usize],
};
(topo, positions)
}
fn cube(creased_top: bool) -> (Mesh, Vec<[f32; 3]>) {
let positions: Vec<[f32; 3]> = vec![
[-1.0, -1.0, -1.0],
[1.0, -1.0, -1.0],
[1.0, 1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, 1.0],
[1.0, -1.0, 1.0],
[1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0],
];
let face_vertex_indices: Vec<u32> = vec![
0, 3, 2, 1, 4, 5, 6, 7, 0, 1, 5, 4, 1, 2, 6, 5, 2, 3, 7, 6, 3, 0, 4, 7, ];
let (edge_vertices, edge_creases) = if creased_top {
(
vec![[4u32, 5], [5, 6], [6, 7], [4, 7]],
vec![f32::INFINITY; 4],
)
} else {
(Vec::new(), Vec::new())
};
let topo = Mesh {
vertex_count: 8,
face_vertex_counts: vec![4; 6],
face_vertex_indices,
edge_vertices,
edge_creases,
vertex_corners: vec![0.0; 8],
};
(topo, positions)
}
type LimitData = (Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<[f32; 3]>);
fn shallow_limit(refiner: &Refiner, positions: &[[f32; 3]]) -> (RefinementResult, LimitData) {
let result = refiner
.refine_uniform(&UniformRefine::from(
NonZeroU8::new(SHALLOW_LEVEL).expect("non-zero"),
))
.expect("shallow refinement");
let refined = result.interpolate(positions);
let limit = result.limit_stencils().expect("limit stencils");
let lim_pos = limit.position.interpolate(&refined);
let tan1 = limit.tangent1.interpolate(&refined);
let tan2 = limit.tangent2.interpolate(&refined);
(result, (lim_pos, tan1, tan2))
}
fn descendants(deep: &Refinement, shallow_count: u32) -> Vec<u32> {
(SHALLOW_LEVEL..DEEP_LEVEL).fold(
(0..shallow_count).collect::<Vec<u32>>(),
|tracked, step| {
let lineage = deep
.level_lineage(step as usize)
.expect("deep refinement has this step");
let parent_count = tracked.iter().copied().max().unwrap_or(0) as usize + 1;
let mut child_of = vec![u32::MAX; parent_count];
lineage
.vertex_origin
.iter()
.enumerate()
.for_each(|(child, origin)| {
if let VertexOrigin::Vertex(parent) = *origin
&& (parent as usize) < parent_count
{
child_of[parent as usize] = child as u32;
}
});
tracked
.iter()
.map(|&p| {
let c = child_of[p as usize];
assert_ne!(c, u32::MAX, "vertex {p} has no vertex-vertex child");
c
})
.collect()
},
)
}
fn averaged_vertex_normal(
topo: &Mesh,
adjacency: &Adjacency,
positions: &[[f32; 3]],
vi: usize,
) -> [f64; 3] {
let start = adjacency.vertex_face_offsets[vi] as usize;
let end = adjacency.vertex_face_offsets[vi + 1] as usize;
let sum = adjacency.vertex_faces[start..end].iter().fold(
[0.0f64; 3],
|acc, &fi| {
let off = (fi * 4) as usize;
let c: Vec<[f64; 3]> = (0..4)
.map(|k| v3(positions[topo.face_vertex_indices[off + k] as usize]))
.collect();
add(acc, cross(sub(c[2], c[0]), sub(c[3], c[1])))
},
);
normalize(sum)
}
fn assert_limit_converges(
topo: Mesh,
positions: &[[f32; 3]],
skip_normal: impl Fn(&RefinementResult, usize) -> bool,
) {
let refiner = Refiner::new(topo, Scheme::CatmullClark, SchemeOptions::default())
.expect("refiner");
let (result, (lim_pos, tan1, tan2)) = shallow_limit(&refiner, positions);
let deep = refiner
.refine_topology(&UniformRefine::from(
NonZeroU8::new(DEEP_LEVEL).expect("non-zero"),
))
.expect("deep refinement");
let deep_pos = deep
.vertex_stencils()
.iter()
.fold(positions.to_vec(), |data, table| table.interpolate(&data));
let descendant = descendants(&deep, result.topology.vertex_count);
let deep_topo = deep.final_topology();
let deep_adjacency = deep.adjacency();
let (lo, hi) = positions.iter().fold(
([f64::MAX; 3], [f64::MIN; 3]),
|(lo, hi), &p| {
let p = v3(p);
(
[lo[0].min(p[0]), lo[1].min(p[1]), lo[2].min(p[2])],
[hi[0].max(p[0]), hi[1].max(p[1]), hi[2].max(p[2])],
)
},
);
let position_tolerance = POSITION_TOLERANCE * length(sub(hi, lo));
for vi in 0..lim_pos.len() {
let target = v3(deep_pos[descendant[vi] as usize]);
let distance = length(sub(v3(lim_pos[vi]), target));
assert!(
distance <= position_tolerance,
"vertex {vi}: limit position {:?} is {distance} from its deep descendant {:?} \
(tolerance {position_tolerance})",
lim_pos[vi],
target,
);
if skip_normal(&result, vi) {
continue;
}
let analytic = cross(v3(tan1[vi]), v3(tan2[vi]));
let numeric = averaged_vertex_normal(
deep_topo,
deep_adjacency,
&deep_pos,
descendant[vi] as usize,
);
let angle = angle_deg(analytic, numeric);
assert!(
angle <= NORMAL_TOLERANCE_DEG,
"vertex {vi}: analytic normal {:?} is {angle} deg from the deep averaged normal {:?}",
normalize(analytic),
numeric,
);
}
}
fn touches_crease(result: &RefinementResult, vi: usize) -> bool {
let start = result.adjacency.vertex_edge_offsets[vi] as usize;
let end = result.adjacency.vertex_edge_offsets[vi + 1] as usize;
result.adjacency.vertex_edges[start..end]
.iter()
.any(|&ei| result.topology.edge_creases[ei as usize] > 0.0)
}
#[test]
fn flat_grid_limit_is_planar_with_winding_normal() {
let (topo, positions) = quad_grid(6, |_, _| 0.0);
let refiner = Refiner::new(topo, Scheme::CatmullClark, SchemeOptions::default())
.expect("refiner");
let (result, (lim_pos, tan1, tan2)) = shallow_limit(&refiner, &positions);
for vi in 0..lim_pos.len() {
assert!(
lim_pos[vi][1].abs() <= 1e-6,
"vertex {vi}: limit position {:?} left the plane",
lim_pos[vi],
);
let normal = normalize(cross(v3(tan1[vi]), v3(tan2[vi])));
assert!(
normal[0].abs() <= 1e-6 && (normal[1] + 1.0).abs() <= 1e-6 && normal[2].abs() <= 1e-6,
"vertex {vi}: limit normal {normal:?} is not the -y winding normal",
);
}
let composed = result
.compose_limit_stencils(positions.len())
.expect("composed limit stencils");
let from_cage = composed.position.interpolate(&positions);
for vi in 0..lim_pos.len() {
let distance = length(sub(v3(from_cage[vi]), v3(lim_pos[vi])));
assert!(
distance <= 1e-5,
"vertex {vi}: composed limit position diverges from the chained one by {distance}",
);
}
}
#[test]
fn cube_limit_converges_to_deep_refinement() {
let (topo, positions) = cube(false);
assert_limit_converges(topo, &positions, |_, _| false);
}
#[test]
fn creased_cube_limit_converges_to_deep_refinement() {
let (topo, positions) = cube(true);
assert_limit_converges(topo, &positions, touches_crease);
}
#[test]
fn open_grid_limit_converges_to_deep_refinement() {
let (topo, positions) = quad_grid(2, |i, j| {
0.4 * (i as f32).sin() - 0.3 * (1.3 * j as f32).cos()
});
assert_limit_converges(topo, &positions, |_, _| false);
}