mod common;
use common::{
Case, POSITION_TOLERANCE, add, angle_deg, cube_case, grid_case, lattice_locations, length,
oracle_at_frame_samples, oracle_samples, recover_ptex_frames, scale, sub, v3,
};
use std::num::NonZeroU8;
use subdiv_kernels::{PatchTable, 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 PatchedCase {
result: RefinementResult,
table: PatchTable,
refined: Vec<[f32; 3]>,
limit_positions: Vec<[f32; 3]>,
}
fn patched_case(case: &Case, level: u8) -> PatchedCase {
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 table = result.patch_table().expect("patch table");
let refined = result.interpolate(&case.positions);
let limit = result.limit_stencils().expect("limit stencils");
let limit_positions = limit.position.interpolate(&refined);
PatchedCase {
result,
table,
refined,
limit_positions,
}
}
fn assert_patches_match_oracle(case: &Case, level: u8, expected_regular: usize) {
let ours = patched_case(case, level);
assert_eq!(
ours.table.faces.len(),
expected_regular,
"unexpected regular patch count",
);
let lattice = oracle_samples(case, &lattice_locations(case.face_count(), level));
let frames = recover_ptex_frames(
&lattice,
level,
&ours.table.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);
for (patch, (frame, patch_rows)) in frames.iter().zip(&rows).enumerate() {
for (uv, &row) in UV_SAMPLES.iter().zip(patch_rows) {
let (position, du, dv) =
ours.table
.eval_with_derivatives(patch, *uv, &ours.refined);
let sample = &oracle[row];
let distance = length(sub(v3(position), sample.position));
assert!(
distance <= POSITION_TOLERANCE,
"patch {patch} at {uv:?}: position {position:?} is {distance} from oracle \
{:?} (ptex face {})",
sample.position,
frame.root,
);
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,
"patch {patch} 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),
"patch {patch} at {uv:?}: {name} magnitude {} vs oracle {}",
length(evaluated),
length(expected),
);
}
}
}
}
#[test]
fn warped_grid_patches_match_opensubdiv_oracle() {
let case = grid_case(6, |i, j| {
0.25 * (i as f32) * (i as f32) - 0.2 * (j as f32) * (i as f32 + 1.0)
});
assert_patches_match_oracle(&case, 1, 100);
}
#[test]
fn cube_patches_match_opensubdiv_oracle() {
assert_patches_match_oracle(&cube_case(false), 2, 72);
}
#[test]
fn creased_cube_patches_match_opensubdiv_oracle() {
assert_patches_match_oracle(&cube_case(true), 2, 56);
}