mod common;
use common::{
Case, POSITION_TOLERANCE, add, angle_deg, cube_case, lattice_locations, length,
oracle_at_frame_samples, oracle_samples, recover_ptex_frames, scale, sub, v3,
};
use std::num::NonZeroU8;
use subdiv_kernels::{
LimitEvaluator, QuadClass, RefinementResult, Refiner, Scheme, UniformRefine,
};
const DERIVATIVE_TOLERANCE_DEG: f64 = 0.5;
const DERIVATIVE_MAGNITUDE_RELATIVE_TOLERANCE: f64 = 1e-2;
const UV_SAMPLES: [[f32; 2]; 8] = [
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
[0.5, 0.0],
[0.5, 0.5],
[0.3, 0.7],
[0.85, 0.15],
];
struct FeatureCase {
result: RefinementResult,
refined: Vec<[f32; 3]>,
limit_positions: Vec<[f32; 3]>,
feature_faces: Vec<u32>,
}
fn feature_case(case: &Case, level: u8) -> FeatureCase {
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 refined = result.interpolate(&case.positions);
let limit = result.limit_stencils().expect("limit stencils");
let limit_positions = limit.position.interpolate(&refined);
let table = result.patch_table().expect("patch table");
let feature_faces: Vec<u32> = (0..result.topology.face_vertex_counts.len() as u32)
.filter(|&face| table.quad_class(face) == QuadClass::Feature)
.collect();
FeatureCase {
result,
refined,
limit_positions,
feature_faces,
}
}
fn smooth_extraordinary(ours: &FeatureCase, vi: u32) -> bool {
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;
end - start != 4
&& !ours.result.adjacency.vertex_is_boundary[vi as usize]
&& ours.result.adjacency.vertex_edges[start..end]
.iter()
.all(|&e| ours.result.topology.edge_creases[e as usize] <= 0.0)
}
fn assert_feature_quads_match_oracle(case: &Case, level: u8, expected_feature: usize) -> usize {
let ours = feature_case(case, level);
assert_eq!(
ours.feature_faces.len(),
expected_feature,
"unexpected feature quad count",
);
let evaluator: LimitEvaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
let lattice = oracle_samples(case, &lattice_locations(case.face_count(), level));
let frames = recover_ptex_frames(
&lattice,
level,
&ours.feature_faces,
&ours.result.face_root,
&ours.result.topology.face_vertex_indices,
&ours.limit_positions,
);
let (oracle, rows) = oracle_at_frame_samples(case, &frames, &UV_SAMPLES);
let mut excluded = 0;
for ((&face, frame), face_rows) in ours.feature_faces.iter().zip(&frames).zip(&rows) {
let corners =
&ours.result.topology.face_vertex_indices[face as usize * 4..face as usize * 4 + 4];
for (k, (uv, &row)) in UV_SAMPLES.iter().zip(face_rows).enumerate() {
let (position, du, dv) = evaluator
.eval_with_derivatives(face, *uv)
.expect("feature eval");
let sample = &oracle[row];
let distance = length(sub(v3(position), sample.position));
assert!(
distance <= POSITION_TOLERANCE,
"face {face} at {uv:?}: position {position:?} is {distance} from oracle \
{:?} (ptex face {})",
sample.position,
frame.root,
);
let weights = evaluator.weights_at(face, *uv).expect("weights row");
let folded = weights.iter().fold([0.0f64; 3], |acc, &(i, w)| {
let p = ours.refined[i as usize];
[
acc[0] + w as f64 * p[0] as f64,
acc[1] + w as f64 * p[1] as f64,
acc[2] + w as f64 * p[2] as f64,
]
});
let distance = length(sub(folded, sample.position));
assert!(
distance <= POSITION_TOLERANCE,
"face {face} at {uv:?}: weights row gives {folded:?}, {distance} from \
oracle {:?}",
sample.position,
);
if k < 4 && smooth_extraordinary(&ours, corners[k]) {
excluded += 1;
continue;
}
let expected_du = add(scale(sample.du, frame.e_u[0]), scale(sample.dv, frame.e_u[1]));
let expected_dv = add(scale(sample.du, frame.e_v[0]), scale(sample.dv, frame.e_v[1]));
for (name, evaluated, expected) in
[("du", v3(du), expected_du), ("dv", v3(dv), expected_dv)]
{
let angle = angle_deg(evaluated, expected);
assert!(
angle <= DERIVATIVE_TOLERANCE_DEG,
"face {face} at {uv:?}: {name} {evaluated:?} is {angle} degrees off the \
chain-ruled oracle derivative {expected:?}",
);
let magnitude_error = (length(evaluated) - length(expected)).abs();
assert!(
magnitude_error
<= DERIVATIVE_MAGNITUDE_RELATIVE_TOLERANCE * length(expected),
"face {face} at {uv:?}: {name} magnitude {} vs oracle {}",
length(evaluated),
length(expected),
);
}
}
}
excluded
}
#[test]
fn cube_feature_quads_match_opensubdiv_oracle() {
let excluded = assert_feature_quads_match_oracle(&cube_case(false), 1, 24);
assert_eq!(excluded, 24, "unexpected smooth-EV exclusion count");
}
#[test]
fn creased_cube_feature_quads_match_opensubdiv_oracle() {
let excluded = assert_feature_quads_match_oracle(&cube_case(true), 2, 40);
assert_eq!(excluded, 12, "unexpected smooth-EV exclusion count");
}
#[test]
fn deep_ev_query_matches_opensubdiv_oracle() {
let case = cube_case(false);
let ours = feature_case(&case, 1);
let evaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
let lattice = oracle_samples(&case, &lattice_locations(case.face_count(), 1));
let frames = recover_ptex_frames(
&lattice,
1,
&ours.feature_faces,
&ours.result.face_root,
&ours.result.topology.face_vertex_indices,
&ours.limit_positions,
);
let (slot, face, k) = ours
.feature_faces
.iter()
.enumerate()
.find_map(|(slot, &face)| {
ours.result.topology.face_vertex_indices
[face as usize * 4..face as usize * 4 + 4]
.iter()
.position(|&c| smooth_extraordinary(&ours, c))
.map(|k| (slot, face, k))
})
.expect("a quad corner at an extraordinary vertex");
let corner = [[0.0f32, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]][k];
let uv = [
if corner[0] == 0.0 { 1e-4 } else { 1.0 - 1e-4 },
if corner[1] == 0.0 { 1e-4 } else { 1.0 - 1e-4 },
];
let (oracle, rows) = oracle_at_frame_samples(&case, &frames[slot..slot + 1], &[uv]);
let position = evaluator.eval(face, uv).expect("deep eval");
let sample = &oracle[rows[0][0]];
let distance = length(sub(v3(position), sample.position));
assert!(
distance <= POSITION_TOLERANCE,
"face {face} at {uv:?}: deep position {position:?} is {distance} from oracle {:?}",
sample.position,
);
}