mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Fractal / Turbulent Noise: lattice cells are two thirds of Scale, as After Effects
After Effects 2026 draws Fractal Noise's Block noise at Scale 100 and Complexity 1 in cells of 66.67 px (the value changes every 66.67 px along a row), not 100 px; Soft Linear noise has the matching correlation length. EffectCraft's lattice is now Scale x 2/3 px (NOISE_CELL), CPU and GPU. Case 904 E03 (Soft Linear, Complexity 4, Scale 100): the noise's autocorrelation at 32 px was 0.68 (AE 0.41), now 0.54; pixel values cannot match (other seeds). Still open: AE's noise spreads about 20 % wider around its mean (std 48 vs 40/255 in E03). Existing projects and the Backgrounds presets with Fractal Noise get finer noise (two thirds of the former cell size). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8d41ef7218
commit
60cf195236
@@ -103,7 +103,7 @@ pub use transition::place_layer;
|
||||
// effectcraft-gpu fx_noise.
|
||||
pub use keying2::{InnerOuterPlan, inner_outer_plan};
|
||||
pub use noise::remove_grain_level;
|
||||
pub use noise3::{FractalGpu, fractal_gpu};
|
||||
pub use noise3::{FractalGpu, NOISE_CELL, fractal_gpu};
|
||||
pub use stylize3::{BumpLight, bump_layer_id};
|
||||
pub use textfx::{TextLayer, text_layer};
|
||||
pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, pmb_plan, posterized_time, time_frames, timewarp_crop, timewarp_plan};
|
||||
|
||||
@@ -60,6 +60,11 @@ pub const BLEND_NORMAL: u32 = 1;
|
||||
/// Index of "Soft Linear" in [`NOISE_TYPES`] (the default, and the GPU path's interpolation).
|
||||
pub const NOISE_SOFT_LINEAR: u32 = 2;
|
||||
|
||||
/// Lattice cell of Fractal / Turbulent Noise in pixels per percent of Scale: After Effects 2026
|
||||
/// draws Block noise at Scale 100 in 66.67 px cells (and Soft Linear noise has the matching
|
||||
/// correlation length), not 100 px.
|
||||
pub const NOISE_CELL: f64 = 2.0 / 3.0;
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Noise & Grain", params, render, gpu: false, float: true }
|
||||
}
|
||||
@@ -236,7 +241,7 @@ impl Fractal {
|
||||
overflow: pr.e("overflow"),
|
||||
invert: pr.b("invert"),
|
||||
rot: rot.sin_cos(),
|
||||
size: [(sw * b.scale).max(1.0) as f32, (sh * b.scale).max(1.0) as f32],
|
||||
size: [(sw * NOISE_CELL * b.scale).max(1.0) as f32, (sh * NOISE_CELL * b.scale).max(1.0) as f32],
|
||||
origin: [ox as f32, oy as f32],
|
||||
center: [cx as f32, cy as f32],
|
||||
perspective: pr.b("transform/perspectiveOffset"),
|
||||
@@ -519,14 +524,34 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn noise_types_differ_and_block_is_piecewise_constant() {
|
||||
let a = run("ec.noise.fractal", &[("noiseType", Value::Enum(0)), ("complexity", num(1.0)), ("transform/scale", num(10.0))]);
|
||||
// Block noise: neighbours inside one 10 px cell share a value.
|
||||
let a = run("ec.noise.fractal", &[("noiseType", Value::Enum(0)), ("complexity", num(1.0)), ("transform/scale", num(15.0))]);
|
||||
// Block noise: neighbours inside one 10 px cell (Scale 15) share a value.
|
||||
assert_eq!(a.get(4, 4), a.get(5, 5));
|
||||
let others: Vec<Image> = (1..4).map(|t| run("ec.noise.fractal", &[("noiseType", Value::Enum(t)), ("transform/scale", num(10.0))])).collect();
|
||||
assert_ne!(others[0], others[1]);
|
||||
assert_ne!(others[1], others[2]);
|
||||
}
|
||||
|
||||
/// After Effects 2026 (R9): Block noise at Scale 100, Complexity 1 changes value every
|
||||
/// 66.67 px along a row.
|
||||
#[test]
|
||||
fn block_noise_cells_are_two_thirds_of_scale_as_in_after_effects() {
|
||||
let img = run_fx(
|
||||
"ec.noise.fractal",
|
||||
&[("noiseType", Value::Enum(0)), ("complexity", num(1.0)), ("blendingMode", Value::Enum(BLEND_NONE))],
|
||||
Image::filled(400, 8, [0.0, 0.0, 0.0, 1.0]),
|
||||
0.0,
|
||||
EffectEnv::default(),
|
||||
)
|
||||
.img;
|
||||
let changes: Vec<i64> = (1..400).filter(|&x| img.get(x, 4) != img.get(x - 1, 4)).collect();
|
||||
assert!(changes.len() >= 4, "{changes:?}");
|
||||
for w in changes.windows(2) {
|
||||
let d = (w[1] - w[0]) as f64;
|
||||
assert!((d - 200.0 / 3.0).abs() <= 1.0, "cell width {d}: {changes:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_width_height_apply_without_uniform_scaling() {
|
||||
let base = &[("complexity", num(1.0)), ("transform/scale", num(8.0))];
|
||||
|
||||
@@ -529,9 +529,19 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let sub = (f("subSettings/subScaling") / 100.0).clamp(0.1, 1.0);
|
||||
p.u[0] = [10, kind, octaves.ceil() as u32, f("evolutionOptions/seed") as u32 ^ 0x51ed];
|
||||
p.u[1] = [pr.b("invert") as u32, pr.e("overflow"), mode, 0];
|
||||
p.f[0] = [f("contrast") as f32 / 100.0, f("brightness") as f32 / 100.0, (sw * b.scale).max(1.0) as f32, (octaves - octaves.floor()) as f32];
|
||||
p.f[0] = [
|
||||
f("contrast") as f32 / 100.0,
|
||||
f("brightness") as f32 / 100.0,
|
||||
(sw * effectcraft_effects::NOISE_CELL * b.scale).max(1.0) as f32,
|
||||
(octaves - octaves.floor()) as f32,
|
||||
];
|
||||
p.f[1] = [ox as f32, oy as f32, sr, cr];
|
||||
p.f[2] = [f("evolution") as f32 / 360.0, f("blend") as f32 / 100.0, (f("opacity") as f32 / 100.0).clamp(0.0, 1.0), (sh * b.scale).max(1.0) as f32];
|
||||
p.f[2] = [
|
||||
f("evolution") as f32 / 360.0,
|
||||
f("blend") as f32 / 100.0,
|
||||
(f("opacity") as f32 / 100.0).clamp(0.0, 1.0),
|
||||
(sh * effectcraft_effects::NOISE_CELL * b.scale).max(1.0) as f32,
|
||||
];
|
||||
p.f[3] = [(f("subSettings/subInfluence") as f32 / 100.0).clamp(0.0, 1.0), 1.0 / sub as f32, 0.0, 0.0];
|
||||
}
|
||||
_ => return None,
|
||||
|
||||
@@ -37,7 +37,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k
|
||||
| Immersive Video | 12 | 12 | — |
|
||||
| Keying | 12 | 12 | Key Cleaner approximated; #533 (wire-removal Frame Offset) is closed |
|
||||
| Matte | 6 | 4 | Mocha shape reads our documented JSON, not Mocha's format |
|
||||
| Noise & Grain | 13 | 12 | Curl Noise (#463); Match Grain on the CPU |
|
||||
| Noise & Grain | 13 | 12 | Fractal / Turbulent Noise lattice matches After Effects 2026 (Scale 100 = 66.7 px cells); the noise values themselves differ (seeds) and Soft Linear contrast is about 20 % lower than AE's; Curl Noise (#463); Match Grain on the CPU |
|
||||
| Obsolete | 9 | 9 | — |
|
||||
| Paint | 1 | 0 | Tablet pressure not read ([hardware-parity.md](hardware-parity.md)) |
|
||||
| Perspective | 10 | 9 | Chromatic Aberrations pan/tilt crash (#580) |
|
||||
@@ -69,6 +69,7 @@ the GPU is performance, not parity.
|
||||
| 2026-10-19 | minor | Radial Blur repeats the edge instead of sampling transparency outside the layer, measured against After Effects 2026 renders (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Venetian Blinds: stripes measured from the layer centre, both band edges Gaussian (σ = Feather / 2), measured against After Effects 2026 (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Radial Wipe: Feather is in pixels on both edges of the wiped wedge (was degrees on the moving edge only), measured against After Effects 2026 (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Fractal / Turbulent Noise: lattice cell = Scale × 2/3 px (Block noise every 66.7 px at Scale 100, as After Effects 2026), CPU and GPU |
|
||||
| 2026-10-12 | minor | Parameters 3,014 to 3,017 (effects.md); #303, #514, #526, #533–#536 marked closed instead of pending |
|
||||
| 2026-10-12 | minor | Effect colour labels (26.5) added in Effect Controls |
|
||||
| 2026-10-12 | minor | Tint's Swap Colors button and agent-drivable command (#963); undo/redo, instance targeting, animated layer-time edits and invalid-input tests |
|
||||
|
||||
Reference in New Issue
Block a user