mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 01:54:38 +00:00
address review: trace over-budget puppet outlines at a coarser resolution; NaN expansion test
This commit is contained in:
@@ -620,23 +620,68 @@ pub fn triangulate(outline: &[[f64; 2]], opts: &MeshOpts) -> Mesh {
|
||||
pub const EXPANSION_RANGE: (f64, f64) = (-100.0, 200.0);
|
||||
/// Most buffer pixels of padding the outline grid takes on each side (Expansion × buffer scale).
|
||||
const MAX_PAD: f64 = 4096.0;
|
||||
/// Most cells the padded outline grid may have (it is expanded with two `f64` distance buffers).
|
||||
const MAX_GRID_CELLS: usize = 1 << 25;
|
||||
/// Most the alpha is shrunk by, per axis, to make the padded grid fit; a layer needing more has no
|
||||
/// outline.
|
||||
const MAX_DOWNSAMPLE: usize = 8;
|
||||
|
||||
/// The outline of the alpha region under `seed` (buffer pixels → layer space via `scale` and
|
||||
/// `offset`), expanded by `expansion` layer pixels (clamped to [`EXPANSION_RANGE`]).
|
||||
/// `offset`), expanded by `expansion` layer pixels (clamped to [`EXPANSION_RANGE`]). When the
|
||||
/// padded grid would pass [`MAX_PAD`] or [`MAX_GRID_CELLS`] (a high-scale layer with a large
|
||||
/// Expansion), the alpha is traced at a coarser resolution instead of being skipped.
|
||||
pub fn outline(alpha: &[f32], w: usize, h: usize, scale: f64, offset: [f64; 2], seed: [f64; 2], expansion: f64) -> Vec<[f64; 2]> {
|
||||
let expansion = if expansion.is_finite() { expansion.clamp(EXPANSION_RANGE.0, EXPANSION_RANGE.1) } else { 0.0 };
|
||||
// Pad so expansion can grow past the buffer edge.
|
||||
let pad = (expansion.max(0.0) * scale).ceil();
|
||||
if !pad.is_finite() || pad > MAX_PAD {
|
||||
return vec![];
|
||||
}
|
||||
let pad = pad as usize + 2;
|
||||
let Some((pw, ph)) = w.checked_add(2 * pad).zip(h.checked_add(2 * pad)) else { return vec![] };
|
||||
let Some(cells) = pw.checked_mul(ph) else { return vec![] };
|
||||
if alpha.len() < w.saturating_mul(h) {
|
||||
return vec![];
|
||||
}
|
||||
let mut on = vec![false; cells];
|
||||
let want = (expansion.max(0.0) * scale).ceil();
|
||||
if !want.is_finite() {
|
||||
return vec![];
|
||||
}
|
||||
for k in 1..=MAX_DOWNSAMPLE {
|
||||
let pad = (want / k as f64).ceil();
|
||||
let (kw, kh) = (w.div_ceil(k), h.div_ceil(k));
|
||||
let fits = pad <= MAX_PAD && {
|
||||
let pad = pad as usize + 2;
|
||||
kw.checked_add(2 * pad).zip(kh.checked_add(2 * pad)).and_then(|(pw, ph)| pw.checked_mul(ph)).is_some_and(|c| c <= MAX_GRID_CELLS)
|
||||
};
|
||||
if !fits {
|
||||
continue;
|
||||
}
|
||||
if k == 1 {
|
||||
return outline_padded(alpha, [w, h], scale, offset, seed, expansion, pad as usize + 2);
|
||||
}
|
||||
let small = shrink(alpha, w, h, k);
|
||||
let kf = k as f64;
|
||||
return outline_padded(&small, [kw, kh], scale / kf, [offset[0] / kf, offset[1] / kf], seed, expansion, pad as usize + 2);
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// `alpha` (`w × h`) with every `k × k` block averaged into one pixel.
|
||||
fn shrink(alpha: &[f32], w: usize, h: usize, k: usize) -> Vec<f32> {
|
||||
let (kw, kh) = (w.div_ceil(k), h.div_ceil(k));
|
||||
let mut out = vec![0.0f32; kw * kh];
|
||||
for y in 0..kh {
|
||||
for x in 0..kw {
|
||||
let (x1, y1) = (((x + 1) * k).min(w), ((y + 1) * k).min(h));
|
||||
let (mut sum, mut n) = (0.0f32, 0u32);
|
||||
for yy in y * k..y1 {
|
||||
for xx in x * k..x1 {
|
||||
sum += alpha[yy * w + xx];
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
out[y * kw + x] = sum / n.max(1) as f32;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn outline_padded(alpha: &[f32], [w, h]: [usize; 2], scale: f64, offset: [f64; 2], seed: [f64; 2], expansion: f64, pad: usize) -> Vec<[f64; 2]> {
|
||||
let (pw, ph) = (w + 2 * pad, h + 2 * pad);
|
||||
let mut on = vec![false; pw * ph];
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
on[(y + pad) * pw + x + pad] = alpha[y * w + x] >= 0.5;
|
||||
|
||||
@@ -115,16 +115,33 @@ fn hostile_expansion_is_clamped() {
|
||||
let max = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], EXPANSION_RANGE.1)).abs();
|
||||
// A non-finite Expansion is taken as zero (the unexpanded outline), not clamped to the maximum.
|
||||
let zero = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], 0.0)).abs();
|
||||
for e in [1e12, f64::MAX, f64::INFINITY] {
|
||||
for e in [1e12, f64::MAX, f64::INFINITY, f64::NAN] {
|
||||
let a = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], e)).abs();
|
||||
let want = if e.is_finite() { max } else { zero };
|
||||
assert!((a - want).abs() < 1.0, "{e}: {a} vs {want}");
|
||||
}
|
||||
assert!(outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], f64::NAN).len() >= 3);
|
||||
assert!(outline(&alpha, w, h, 1e9, [0.0; 2], [20.0, 20.0], 100.0).is_empty());
|
||||
let _ = outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], -1e12);
|
||||
}
|
||||
|
||||
/// A high-scale layer with a large Expansion is traced at a coarser resolution, not skipped, and a
|
||||
/// tiny buffer with a huge padded grid stays within the grid budget.
|
||||
#[test]
|
||||
fn high_scale_expansion_is_traced_coarser_instead_of_skipped() {
|
||||
let (w, h) = (40, 40);
|
||||
let alpha = disc_alpha(w, h, [20.0, 20.0], 10.0);
|
||||
// Scale 32, Expansion 130: 4160 px of padding, past MAX_PAD, so it is shrunk by 2.
|
||||
let scale = 32.0;
|
||||
let o = outline(&alpha, w, h, scale, [0.0; 2], [20.0, 20.0], 130.0);
|
||||
let area = signed_area(&o).abs();
|
||||
// The disc has radius 10 / 32 layer px; expanded by 130.
|
||||
let r = 10.0 / scale + 130.0;
|
||||
assert!((area - std::f64::consts::PI * r * r).abs() < 0.03 * std::f64::consts::PI * r * r, "{area}");
|
||||
// One pixel at scale 20.48 and Expansion 1e12 (clamped): its 8197 px square grid is over budget.
|
||||
let o = outline(&[1.0], 1, 1, 20.48, [0.0; 2], [0.5, 0.5], 1e12);
|
||||
assert!(o.len() >= 3);
|
||||
}
|
||||
|
||||
fn rect_mesh(w: f64, h: f64, tris: f64) -> Mesh {
|
||||
triangulate(&[[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]], &MeshOpts { triangles: tris, density: 50.0, expansion: 0.0 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user