mod common;
use common::{
Case, OracleSample, cross, cube_case, dot, fan_case, lattice_locations, length, normalize,
oracle_samples, spoked_cube_case, sub, v3,
};
use std::num::NonZeroU8;
use subdiv_kernels::{
LimitStencils, RefinementResult, Refiner, Scheme, SectoredLimitStencils, UniformRefine,
};
const LEVEL: u8 = 2;
const POSITION_TOLERANCE: f64 = 1e-4;
const NORMAL_TOLERANCE_DEG: f64 = 0.5;
struct Sample {
position: [f64; 3],
normal: Option<[f64; 3]>,
}
fn oracle_lattice(case: &Case) -> Vec<OracleSample> {
oracle_samples(case, &lattice_locations(case.face_count(), LEVEL))
}
fn our_samples(case: &Case) -> (Vec<Sample>, Vec<bool>) {
let (result, limit) = our_refinement(case);
let positions = limit.position.interpolate(&case.positions);
let tan1 = limit.tangent1.interpolate(&case.positions);
let tan2 = limit.tangent2.interpolate(&case.positions);
let samples = positions
.iter()
.zip(tan1.iter().zip(&tan2))
.map(|(&p, (&t1, &t2))| Sample {
position: v3(p),
normal: Some(normalize(cross(v3(t1), v3(t2)))),
})
.collect();
let ambiguous = (0..result.topology.vertex_count as usize)
.map(|vi| {
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)
})
.collect();
(samples, ambiguous)
}
fn our_refinement(case: &Case) -> (RefinementResult, LimitStencils) {
let refiner =
Refiner::new(case.topology(), Scheme::CatmullClark, case.options).expect("refiner");
let result = refiner
.refine_uniform(&UniformRefine::from(NonZeroU8::new(LEVEL).expect("non-zero")))
.expect("refinement");
let limit = result
.compose_limit_stencils(case.positions.len())
.expect("composed limit stencils");
(result, limit)
}
fn assert_limit_matches_oracle(case: &Case) {
let (ours, ambiguous) = our_samples(case);
let oracle = oracle_lattice(case);
let candidates: Vec<Vec<usize>> = ours
.iter()
.enumerate()
.map(|(vi, sample)| {
let near: Vec<usize> = oracle
.iter()
.enumerate()
.filter(|(_, o)| length(sub(o.position, sample.position)) <= POSITION_TOLERANCE)
.map(|(k, _)| k)
.collect();
assert!(
!near.is_empty(),
"vertex {vi}: no oracle sample within {POSITION_TOLERANCE} of limit position {:?} \
(nearest at {:?})",
sample.position,
oracle
.iter()
.map(|o| o.position)
.min_by(|a, b| {
let (da, db) = (length(sub(*a, sample.position)),
length(sub(*b, sample.position)));
da.partial_cmp(&db).expect("finite distances")
}),
);
near
})
.collect();
let votes: f64 = ours
.iter()
.zip(&candidates)
.zip(&ambiguous)
.filter(|&(_, &skip)| !skip)
.map(|((sample, near), _)| {
let normal = sample.normal.expect("our samples always carry normals");
near.iter()
.filter_map(|&k| oracle[k].normal().map(|n| dot(normal, n)))
.fold(0.0f64, |best, d| if d.abs() > best.abs() { d } else { best })
})
.sum();
let sign = if votes >= 0.0 { 1.0 } else { -1.0 };
let min_alignment = (NORMAL_TOLERANCE_DEG * std::f64::consts::PI / 180.0).cos();
for (vi, ((sample, near), &skip)) in ours.iter().zip(&candidates).zip(&ambiguous).enumerate() {
if skip {
continue;
}
let normal = sample.normal.expect("our samples always carry normals");
let best = near
.iter()
.filter_map(|&k| oracle[k].normal().map(|n| sign * dot(normal, n)))
.fold(f64::MIN, f64::max);
assert!(
best >= min_alignment,
"vertex {vi} at {:?}: analytic normal {normal:?} matches no coincident oracle normal \
(best alignment {best}, need {min_alignment}; candidates {:?})",
sample.position,
near.iter().map(|&k| oracle[k].normal()).collect::<Vec<_>>(),
);
}
}
struct SectoredSamples {
result: RefinementResult,
sectored: SectoredLimitStencils,
positions: Vec<[f32; 3]>,
tan1: Vec<[f32; 3]>,
tan2: Vec<[f32; 3]>,
}
fn sectored_samples(case: &Case) -> SectoredSamples {
let (result, _) = our_refinement(case);
let sectored = result
.compose_sectored_limit_stencils(case.positions.len())
.expect("composed sectored limit stencils");
let positions = sectored.position.interpolate(&case.positions);
let tan1 = sectored.tangent1.interpolate(&case.positions);
let tan2 = sectored.tangent2.interpolate(&case.positions);
SectoredSamples {
result,
sectored,
positions,
tan1,
tan2,
}
}
fn assert_sectored_limit_matches_oracle(case: &Case) -> (SectoredSamples, Vec<(u32, u32)>) {
let ours = sectored_samples(case);
let oracle = oracle_lattice(case);
let topo = &ours.result.topology;
let side = (1usize << LEVEL) + 1;
let per_face = side * side;
let row_corners = ours.sectored.corner_sector.iter().fold(
vec![0u32; ours.tan1.len()],
|mut counts, &row| {
counts[row as usize] += 1;
counts
},
);
let pinned: Vec<bool> = (0..topo.vertex_count as usize)
.map(|vi| {
let start = ours.result.adjacency.vertex_edge_offsets[vi] as usize;
let end = ours.result.adjacency.vertex_edge_offsets[vi + 1] as usize;
let sharp = ours.result.adjacency.vertex_edges[start..end]
.iter()
.filter(|&&ei| topo.edge_creases[ei as usize] > 0.0)
.count();
sharp >= 3 || topo.vertex_corners[vi] > 0.0
})
.collect();
let matches: Vec<(usize, u32, u32, usize)> = topo
.face_vertex_indices
.iter()
.enumerate()
.map(|(corner, &vertex)| {
let face = corner / 4;
let base = ours.result.face_root[face] as usize * per_face;
let position = v3(ours.positions[vertex as usize]);
let near: Vec<usize> = (base..base + per_face)
.filter(|&k| length(sub(oracle[k].position, position)) <= POSITION_TOLERANCE)
.collect();
assert_eq!(
near.len(),
1,
"corner {corner} (face {face}, vertex {vertex}): expected exactly one lattice \
sample of ptex face {} at limit position {position:?}, found {}",
ours.result.face_root[face],
near.len(),
);
let row = ours.sectored.corner_sector[corner];
(corner, vertex, row, near[0])
})
.collect();
let compared = |&(_, vertex, row, sample): &(usize, u32, u32, usize)| -> Option<[f64; 3]> {
let multi_face_pinned = pinned[vertex as usize] && row_corners[row as usize] > 1;
if multi_face_pinned {
None
} else {
oracle[sample].normal()
}
};
let votes: f64 = matches
.iter()
.filter_map(|m| {
compared(m).map(|oracle_normal| {
let normal = normalize(cross(v3(ours.tan1[m.2 as usize]), v3(ours.tan2[m.2 as usize])));
dot(normal, oracle_normal)
})
})
.sum();
let sign = if votes >= 0.0 { 1.0 } else { -1.0 };
let min_alignment = (NORMAL_TOLERANCE_DEG * std::f64::consts::PI / 180.0).cos();
let mut rows_compared: Vec<(u32, u32)> = Vec::new();
for m in &matches {
let (corner, vertex, row, sample) = *m;
let Some(oracle_normal) = compared(m) else {
continue;
};
let normal = normalize(cross(v3(ours.tan1[row as usize]), v3(ours.tan2[row as usize])));
let alignment = sign * dot(normal, oracle_normal);
assert!(
alignment >= min_alignment,
"corner {corner} (vertex {vertex}, sector row {row}): sector normal {normal:?} \
disagrees with its face's oracle normal {oracle_normal:?} at sample {sample} \
(alignment {alignment}, need {min_alignment})",
);
rows_compared.push((vertex, row));
}
assert!(
!rows_compared.is_empty(),
"no corner normals were compared at all",
);
rows_compared.sort_unstable();
rows_compared.dedup();
(ours, rows_compared)
}
fn crease_vertices(ours: &SectoredSamples) -> Vec<u32> {
(0..ours.result.topology.vertex_count)
.filter(|&vi| {
let start = ours.result.adjacency.vertex_edge_offsets[vi as usize] as usize;
let end = ours.result.adjacency.vertex_edge_offsets[vi as usize + 1] as usize;
ours.result.adjacency.vertex_edges[start..end]
.iter()
.filter(|&&ei| ours.result.topology.edge_creases[ei as usize] > 0.0)
.count()
== 2
})
.collect()
}
#[test]
fn cube_limit_matches_opensubdiv_oracle() {
assert_limit_matches_oracle(&cube_case(false));
}
#[test]
fn creased_cube_limit_matches_opensubdiv_oracle() {
assert_limit_matches_oracle(&cube_case(true));
}
#[test]
fn boundary_fan_limit_matches_opensubdiv_oracle() {
assert_limit_matches_oracle(&fan_case());
}
#[test]
fn cube_sectored_limit_matches_opensubdiv_oracle_per_corner() {
let (ours, _) = assert_sectored_limit_matches_oracle(&cube_case(false));
assert_eq!(ours.tan1.len(), ours.result.topology.vertex_count as usize);
}
#[test]
fn creased_cube_sectored_limit_matches_opensubdiv_oracle_per_corner() {
let (ours, compared) = assert_sectored_limit_matches_oracle(&cube_case(true));
let rim = crease_vertices(&ours);
assert_eq!(rim.len(), 16, "unexpected rim vertex count");
let both_sides = rim
.iter()
.filter(|&&vi| compared.iter().filter(|&&(v, _)| v == vi).count() >= 2)
.count();
assert!(
both_sides >= rim.len() - 4,
"only {both_sides} of {} rim vertices had both sector normals oracle-matched",
rim.len(),
);
}
#[test]
fn boundary_fan_sectored_limit_matches_opensubdiv_oracle_per_corner() {
let (ours, _) = assert_sectored_limit_matches_oracle(&fan_case());
assert_eq!(ours.tan1.len(), ours.result.topology.vertex_count as usize);
}
#[test]
fn spoked_cube_sectored_limit_matches_opensubdiv_oracle_per_corner() {
let (case, center) = spoked_cube_case();
let center_position = v3(case.positions[center as usize]);
let (ours, compared) = assert_sectored_limit_matches_oracle(&case);
let pinned = (0..ours.result.topology.vertex_count as usize)
.find(|&vi| length(sub(v3(ours.positions[vi]), center_position)) < 1e-6)
.expect("pinned vertex descendant") as u32;
let pinned_rows: Vec<u32> = compared
.iter()
.filter(|&&(v, _)| v == pinned)
.map(|&(_, row)| row)
.collect();
assert_eq!(
pinned_rows.len(),
2,
"expected the two single-face sectors of the pinned vertex to be compared: {pinned_rows:?}",
);
let spokes = crease_vertices(&ours);
assert!(!spokes.is_empty(), "expected crease vertices along the spokes");
let both_sides = spokes
.iter()
.filter(|&&vi| compared.iter().filter(|&&(v, _)| v == vi).count() >= 2)
.count();
assert!(
both_sides >= spokes.len() - 1,
"only {both_sides} of {} spoke vertices had both sector normals oracle-matched",
spokes.len(),
);
}