mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-03 06:52:21 -06:00
Effects: cap Camera Lens Blur and median radii
Camera Lens Blur built its iris table (one row per pixel of radius) from an uncapped Blur Radius before any size check, and the median window is filled (2r + 1)^2 per row from an uncapped Radius, on the CPU and the GPU. The radius is now capped where the work is built: 2000 buffer pixels for the lens iris (UI max 500) and 512 for the median window (UI max 255), on both paths.
This commit is contained in:
@@ -228,8 +228,21 @@ fn compound_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Camera Lens Blur
|
||||
|
||||
/// Half-open horizontal spans (dy, x_left, x_right) of a polygonal / round iris of radius `r`.
|
||||
/// Largest Camera Lens Blur radius in buffer pixels. Blur Radius goes to 500 in the UI; a project
|
||||
/// or expression value far beyond it would otherwise build an iris table of one row per pixel of
|
||||
/// radius (and sum that many rows per output pixel) before any image size check.
|
||||
pub const CAMERA_LENS_MAX_RADIUS: f64 = 2000.0;
|
||||
|
||||
/// Camera Lens Blur's iris radius in buffer pixels: Blur Radius at the buffer's `scale`, capped
|
||||
/// at [`CAMERA_LENS_MAX_RADIUS`]. Shared by the CPU and GPU paths.
|
||||
pub fn camera_lens_radius(ctx: &EffectCtx, scale: f64) -> f64 {
|
||||
(ctx.params.f("blurRadius").max(0.0) * scale).clamp(0.0, CAMERA_LENS_MAX_RADIUS)
|
||||
}
|
||||
|
||||
/// Half-open horizontal spans (dy, x_left, x_right) of a polygonal / round iris of radius `r`
|
||||
/// (capped at [`CAMERA_LENS_MAX_RADIUS`]).
|
||||
fn iris_spans(r: f64, sides: u32, rotation_deg: f64, roundness: f64) -> Vec<(i64, i64, i64)> {
|
||||
let r = if r.is_nan() { 0.0 } else { r.clamp(0.0, CAMERA_LENS_MAX_RADIUS) };
|
||||
let ri = r.ceil() as i64;
|
||||
let rot = rotation_deg.to_radians();
|
||||
let verts: Vec<(f64, f64)> = (0..sides)
|
||||
@@ -302,7 +315,7 @@ pub fn camera_lens_plain(ctx: &EffectCtx) -> bool {
|
||||
}
|
||||
|
||||
fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
|
||||
let r = camera_lens_radius(ctx, b.scale);
|
||||
if r < 0.5 {
|
||||
return b;
|
||||
}
|
||||
@@ -910,6 +923,19 @@ mod tests {
|
||||
assert!(out.img.get(15 + o, 10 + d)[0] < 0.9, "bright map blurs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iris_spans_cap_a_huge_radius() {
|
||||
// A project's Blur Radius far beyond the UI's 500 builds a bounded table.
|
||||
let max_rows = 2 * CAMERA_LENS_MAX_RADIUS as usize + 1;
|
||||
assert!(iris_spans(1e12, 6, 0.0, 0.0).len() <= max_rows);
|
||||
assert!(iris_spans(f64::INFINITY, 0, 0.0, 0.0).len() <= max_rows);
|
||||
assert_eq!(iris_spans(f64::NAN, 0, 0.0, 0.0), vec![(0, 0, 0)]);
|
||||
let params = Params { values: [("blurRadius".into(), num(1e15))].into_iter().collect() };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [4.0, 4.0], seed: 1, adjustment: false, env: Default::default() };
|
||||
assert_eq!(camera_lens_radius(&ctx, 1.0), CAMERA_LENS_MAX_RADIUS);
|
||||
assert!(camera_lens_spans(&ctx, camera_lens_radius(&ctx, 1.0)).0.len() <= max_rows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iris_spans_hexagon_is_symmetric() {
|
||||
let s = iris_spans(10.0, 6, 0.0, 0.0);
|
||||
|
||||
@@ -95,14 +95,14 @@ pub use sim::particle_state;
|
||||
pub use sim3::{PgBlit, PgPlan, playground_plan, playground_state};
|
||||
pub use stylize2::strobe_on;
|
||||
// CPU helpers the GPU kernels share (effectcraft-gpu).
|
||||
pub use blur2::{camera_lens_plain, camera_lens_spans, radial_fast_pad};
|
||||
pub use blur2::{CAMERA_LENS_MAX_RADIUS, camera_lens_plain, camera_lens_radius, camera_lens_spans, radial_fast_pad};
|
||||
pub use generate::{four_color_softening, gen_mode};
|
||||
pub use generate2::pattern_kind as cell_pattern_kind;
|
||||
pub use generate2::{BoltSeg, Contour, Ghost, RadioPlan, Wave, flare_ghosts, lightning_segments, radio_plan};
|
||||
pub use generate3::{
|
||||
Coverage, MarksPlan, PaintPlan, Seg, eyedropper_color, marks_plan, paint_bucket_region, paint_plan, plan_reads_pixels, poly_segs, scribble_style,
|
||||
};
|
||||
pub use noise::GrainLook;
|
||||
pub use noise::{GrainLook, MEDIAN_MAX_RADIUS};
|
||||
pub use textfx::{BoltPlan, TextLook, TextPass, bolt_plan, text_passes};
|
||||
pub use transition::place_layer;
|
||||
// effectcraft-gpu fx_noise.
|
||||
|
||||
@@ -348,9 +348,16 @@ fn quant(v: f32) -> u16 {
|
||||
(v.clamp(0.0, 1.0) * (NB - 1) as f32).round() as u16
|
||||
}
|
||||
|
||||
/// Largest median window radius (buffer pixels) for Median, Median (Legacy), Dust & Scratches
|
||||
/// and the other median users. Radius goes to 255 in the UI; filling one (2r + 1)² window per
|
||||
/// row costs the square of the radius, so a project value far beyond it is held here (also
|
||||
/// keeping the window's pixel count well inside `u32` / `i32` on the GPU).
|
||||
pub const MEDIAN_MAX_RADIUS: usize = 512;
|
||||
|
||||
/// Per-channel median over a (2r+1)² window of the premultiplied channels (values quantised to
|
||||
/// 512 levels in 0..1, edges repeated).
|
||||
pub(crate) fn median_image(img: &Image, r: usize) -> Image {
|
||||
let r = r.min(MEDIAN_MAX_RADIUS);
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
if r == 0 || w == 0 || h == 0 {
|
||||
return img.clone();
|
||||
@@ -849,6 +856,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_radius_is_capped() {
|
||||
// A project's Radius far beyond the UI's 255 takes the capped window, not a
|
||||
// (2r + 1)² fill per row (or an overflow) on a tiny image.
|
||||
let mut img = Image::filled(4, 3, [0.2, 0.3, 0.4, 1.0]);
|
||||
img.set(1, 1, [1.0, 1.0, 1.0, 1.0]);
|
||||
let capped = median_image(&img, MEDIAN_MAX_RADIUS);
|
||||
assert_eq!(median_image(&img, usize::MAX).data, capped.data);
|
||||
assert_eq!(median_image(&img, MEDIAN_MAX_RADIUS + 1000).data, capped.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_matches_brute_force() {
|
||||
let (w, h) = (9u32, 7u32);
|
||||
|
||||
@@ -133,7 +133,7 @@ fn radial_fast(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
}
|
||||
|
||||
fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
|
||||
let r = effectcraft_effects::camera_lens_radius(ctx, b.scale);
|
||||
if r < 0.5 {
|
||||
return Some(b);
|
||||
}
|
||||
|
||||
@@ -202,6 +202,8 @@ pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) ->
|
||||
|
||||
/// noise::median_image (of the straight colour with alpha 1 when `straight`).
|
||||
pub(crate) fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage {
|
||||
// As on the CPU: the window's radius is capped, so its first fill stays bounded.
|
||||
let r = r.min(effectcraft_effects::MEDIAN_MAX_RADIUS);
|
||||
let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None);
|
||||
// Preserve the CPU's rounded level / 511 values before the strict Dust & Scratches
|
||||
// threshold. GPU division (even residual-corrected) may differ by one ulp.
|
||||
|
||||
Reference in New Issue
Block a user