mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 16:33:01 -06:00
Merge main into fix/2022-render-banding (#1631)
Resolve the export tests' module list (banding and h264_gpu_yuv both stay). #1631's yuv420_frame checked Render Settings Color Depth itself; that depth is now the project's depth for the whole render, so the GPU YUV path is taken only for 8 bpc renders. Above 8 bpc frames take the RGBA path, which dithers, so H.264 exports don't band either. Test: renders_above_8_bpc_skip_the_planes.
This commit is contained in:
@@ -247,7 +247,9 @@ pub fn radial_blur(img: &Image, center: (f64, f64), amount: f64, zoom: bool) ->
|
||||
let (s, c) = (t * amount).to_radians().sin_cos();
|
||||
(center.0 + vx * c - vy * s, center.1 + vx * s + vy * c)
|
||||
};
|
||||
let p = img.sample_bilinear(sx, sy);
|
||||
// Samples outside the image repeat its edge: After Effects' Radial Blur keeps a
|
||||
// layer's edges (cases 713, 829) instead of fading them toward transparency.
|
||||
let p = img.sample_bilinear(sx.clamp(0.5, (img.width as f64 - 0.5).max(0.5)), sy.clamp(0.5, (img.height as f64 - 0.5).max(0.5)));
|
||||
for ch in 0..4 {
|
||||
acc[ch] += p[ch];
|
||||
}
|
||||
|
||||
@@ -607,38 +607,44 @@ pub struct FillInput {
|
||||
pub references: Vec<(usize, Image)>,
|
||||
}
|
||||
|
||||
/// Trace pixel (x, y) of frame `t` through `flows` (`step` = +1 forward with `fwd`, −1 backward
|
||||
/// with `bwd`) to the nearest frame where it is visible: (sampled colour, frames travelled).
|
||||
fn trace(
|
||||
t: usize,
|
||||
/// Shared frame and flow data for tracing pixels through a sequence.
|
||||
struct TraceContext<'a> {
|
||||
fwd: &'a [Flow],
|
||||
bwd: &'a [Flow],
|
||||
known: &'a (dyn Fn(usize, i64, i64) -> bool + Sync),
|
||||
frames: &'a [&'a Image],
|
||||
max: usize,
|
||||
}
|
||||
|
||||
/// Starting frame, point, and direction for a flow trace.
|
||||
struct TraceStart {
|
||||
frame: usize,
|
||||
x: f64,
|
||||
y: f64,
|
||||
step: i64,
|
||||
fwd: &[Flow],
|
||||
bwd: &[Flow],
|
||||
known: &dyn Fn(usize, i64, i64) -> bool,
|
||||
frames: &[&Image],
|
||||
max: usize,
|
||||
) -> Option<([f32; 4], usize)> {
|
||||
let n = frames.len();
|
||||
let (w, h) = (frames[0].width as f64, frames[0].height as f64);
|
||||
let (mut px, mut py) = (x, y);
|
||||
let mut k = t as i64;
|
||||
for d in 1..=max {
|
||||
let v = if step > 0 {
|
||||
}
|
||||
|
||||
/// Trace a point to the nearest visible frame: (sampled colour, frames travelled).
|
||||
fn trace(context: &TraceContext<'_>, start: TraceStart) -> Option<([f32; 4], usize)> {
|
||||
let n = context.frames.len();
|
||||
let (w, h) = (context.frames[0].width as f64, context.frames[0].height as f64);
|
||||
let (mut px, mut py) = (start.x, start.y);
|
||||
let mut k = start.frame as i64;
|
||||
for d in 1..=context.max {
|
||||
let v = if start.step > 0 {
|
||||
if k as usize + 1 >= n {
|
||||
return None;
|
||||
}
|
||||
fwd[k as usize].at(px, py)
|
||||
context.fwd[k as usize].at(px, py)
|
||||
} else {
|
||||
if k == 0 {
|
||||
return None;
|
||||
}
|
||||
bwd[k as usize - 1].at(px, py)
|
||||
context.bwd[k as usize - 1].at(px, py)
|
||||
};
|
||||
px += v[0] as f64;
|
||||
py += v[1] as f64;
|
||||
k += step;
|
||||
k += start.step;
|
||||
if px < 0.0 || py < 0.0 || px >= w || py >= h {
|
||||
return None;
|
||||
}
|
||||
@@ -646,9 +652,9 @@ fn trace(
|
||||
let (x0, y0) = ((px - 0.5).floor() as i64, (py - 0.5).floor() as i64);
|
||||
let ok = [(x0, y0), (x0 + 1, y0), (x0, y0 + 1), (x0 + 1, y0 + 1)]
|
||||
.iter()
|
||||
.all(|&(xx, yy)| known(k as usize, xx.clamp(0, w as i64 - 1), yy.clamp(0, h as i64 - 1)));
|
||||
.all(|&(xx, yy)| (context.known)(k as usize, xx.clamp(0, w as i64 - 1), yy.clamp(0, h as i64 - 1)));
|
||||
if ok {
|
||||
return Some((frames[k as usize].sample_bilinear_clamped(px, py), d));
|
||||
return Some((context.frames[k as usize].sample_bilinear_clamped(px, py), d));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -697,6 +703,7 @@ pub fn fill_sequence(input: &FillInput, o: &FillOpts, progress: &mut dyn FnMut(u
|
||||
// 2. Propagation (and the lighting-correction border ring).
|
||||
let refs: Vec<&Image> = frames.iter().collect();
|
||||
let known = |k: usize, x: i64, y: i64| !holes[k][y as usize * w + x as usize];
|
||||
let trace_context = TraceContext { fwd: &fwd, bwd: &bwd, known: &known, frames: &refs, max: n };
|
||||
let ring_r = 2usize;
|
||||
let propagated: Vec<(Image, Vec<bool>)> = (0..n)
|
||||
.into_par_iter()
|
||||
@@ -708,8 +715,8 @@ pub fn fill_sequence(input: &FillInput, o: &FillOpts, progress: &mut dyn FnMut(u
|
||||
}
|
||||
let sample = |i: usize| -> Option<[f32; 4]> {
|
||||
let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
|
||||
let f = trace(t, x, y, 1, &fwd, &bwd, &known, &refs, n);
|
||||
let b = trace(t, x, y, -1, &fwd, &bwd, &known, &refs, n);
|
||||
let f = trace(&trace_context, TraceStart { frame: t, x, y, step: 1 });
|
||||
let b = trace(&trace_context, TraceStart { frame: t, x, y, step: -1 });
|
||||
match (f, b) {
|
||||
(Some((a, da)), Some((c, dc))) => {
|
||||
let (wa, wc) = (1.0 / da as f32, 1.0 / dc as f32);
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod flow;
|
||||
pub mod inpaint;
|
||||
pub mod scopes;
|
||||
pub mod warp;
|
||||
pub mod yuv;
|
||||
|
||||
use effectcraft_color::{BlendMode, blend_pixel};
|
||||
use rayon::prelude::*;
|
||||
|
||||
+22
-21
@@ -203,22 +203,17 @@ fn union(r: [usize; 4], q: [usize; 4]) -> [usize; 4] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast affine rows: `blend(dst, src, x, y)` is only called for visible source pixels. Returns
|
||||
/// the rectangle of the pixels it blended.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn affine_rows(
|
||||
dst: &mut Image,
|
||||
img: &Image,
|
||||
inv: &Mat3,
|
||||
/// Sampling, blending and destination bounds for an affine row operation.
|
||||
struct AffineRows {
|
||||
sampling: Sampling,
|
||||
op: f32,
|
||||
opacity: f32,
|
||||
quantize: Option<f32>,
|
||||
x0: usize,
|
||||
x1: usize,
|
||||
y0: usize,
|
||||
y1: usize,
|
||||
blend: impl Fn(Px, Px, u32, u32) -> Px + Sync,
|
||||
) -> [usize; 4] {
|
||||
bounds: [usize; 4],
|
||||
}
|
||||
|
||||
/// Blend visible source samples and return the rectangle of changed pixels.
|
||||
fn affine_rows(dst: &mut Image, img: &Image, inv: &Mat3, rows: AffineRows, blend: impl Fn(Px, Px, u32, u32) -> Px + Sync) -> [usize; 4] {
|
||||
let AffineRows { sampling, opacity: op, quantize, bounds: [x0, x1, y0, y1] } = rows;
|
||||
let levels = quantize.map(|l| (l, 1.0 / l));
|
||||
let w = dst.width as usize;
|
||||
let m = inv.0;
|
||||
@@ -363,22 +358,28 @@ fn composite_warp_impl(
|
||||
if let Some(k) = accumulate
|
||||
&& affine
|
||||
{
|
||||
let r = affine_rows(dst, img, &inv, opts.sampling, 1.0, None, x0, x1, y0, y1, |d, s, _, _| {
|
||||
let r = affine_rows(dst, img, &inv, AffineRows { sampling: opts.sampling, opacity: 1.0, quantize: None, bounds: [x0, x1, y0, y1] }, |d, s, _, _| {
|
||||
[d[0] + s[0] * k, d[1] + s[1] * k, d[2] + s[2] * k, d[3] + s[3] * k]
|
||||
});
|
||||
return changed(r);
|
||||
}
|
||||
if allow_fast && affine && !mode.is_stencil() {
|
||||
let r = match mode {
|
||||
BlendMode::Normal => affine_rows(dst, img, &inv, opts.sampling, op, quantize, x0, x1, y0, y1, |d, s, _, _| {
|
||||
let k = 1.0 - s[3];
|
||||
[s[0] + d[0] * k, s[1] + d[1] * k, s[2] + d[2] * k, s[3] + d[3] * k]
|
||||
}),
|
||||
BlendMode::Normal => {
|
||||
affine_rows(dst, img, &inv, AffineRows { sampling: opts.sampling, opacity: op, quantize, bounds: [x0, x1, y0, y1] }, |d, s, _, _| {
|
||||
let k = 1.0 - s[3];
|
||||
[s[0] + d[0] * k, s[1] + d[1] * k, s[2] + d[2] * k, s[3] + d[3] * k]
|
||||
})
|
||||
}
|
||||
BlendMode::Dissolve | BlendMode::DancingDissolve => {
|
||||
let seed = opts.seed;
|
||||
affine_rows(dst, img, &inv, opts.sampling, op, quantize, x0, x1, y0, y1, |d, s, x, y| blend_pixel(mode, d, s, hash_noise(x, y, seed)))
|
||||
affine_rows(dst, img, &inv, AffineRows { sampling: opts.sampling, opacity: op, quantize, bounds: [x0, x1, y0, y1] }, |d, s, x, y| {
|
||||
blend_pixel(mode, d, s, hash_noise(x, y, seed))
|
||||
})
|
||||
}
|
||||
_ => affine_rows(dst, img, &inv, opts.sampling, op, quantize, x0, x1, y0, y1, |d, s, _, _| blend_pixel(mode, d, s, 0.5)),
|
||||
_ => affine_rows(dst, img, &inv, AffineRows { sampling: opts.sampling, opacity: op, quantize, bounds: [x0, x1, y0, y1] }, |d, s, _, _| {
|
||||
blend_pixel(mode, d, s, 0.5)
|
||||
}),
|
||||
};
|
||||
return changed(r);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! 8-bit BT.709 limited-range 4:2:0, the H.264 export's input. Shared by the export and by the
|
||||
//! GPU compositor's tests, which hold its kernel to this function.
|
||||
|
||||
use rayon::prelude::*;
|
||||
|
||||
/// `v / 255` for every 8-bit `v` (the same values as dividing).
|
||||
pub const UNIT8: [f32; 256] = {
|
||||
let mut t = [0.0; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
t[i] = i as f32 / 255.0;
|
||||
i += 1;
|
||||
}
|
||||
t
|
||||
};
|
||||
|
||||
/// `v.round()` for `0 <= v < 2^23`, the range the colour conversions round in: truncation and
|
||||
/// the fraction are exact there, so this is `f32::round` without its libm call.
|
||||
#[inline(always)]
|
||||
pub fn round_pos(v: f32) -> f32 {
|
||||
let t = v as u32 as f32;
|
||||
if v - t >= 0.5 { t + 1.0 } else { t }
|
||||
}
|
||||
|
||||
/// BT.709 limited-range 8-bit 4:2:0 from RGBA8 (2×2 chroma average; alpha ignored).
|
||||
pub fn rgba_to_yuv420(rgba: &[u8], w: usize, h: usize, y: &mut Vec<u8>, u: &mut Vec<u8>, v: &mut Vec<u8>) {
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
y.resize(w * h, 0);
|
||||
u.resize(cw * ch, 0);
|
||||
v.resize(cw * ch, 0);
|
||||
y.par_chunks_mut(w * 2).zip(u.par_chunks_mut(cw).zip(v.par_chunks_mut(cw))).enumerate().for_each(|(cy, (yr, (ur, vr)))| {
|
||||
let mut acc = vec![(0f32, 0f32, 0f32); cw];
|
||||
for (dy, yrow) in yr.chunks_exact_mut(w).enumerate() {
|
||||
let src = &rgba[(cy * 2 + dy) * w * 4..][..w * 4];
|
||||
for (x, (s, yo)) in src.as_chunks::<4>().0.iter().zip(yrow.iter_mut()).enumerate() {
|
||||
let (r, g, b) = (UNIT8[s[0] as usize], UNIT8[s[1] as usize], UNIT8[s[2] as usize]);
|
||||
let l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
*yo = round_pos(16.0 + 219.0 * l).clamp(1.0, 254.0) as u8;
|
||||
let a = &mut acc[x / 2];
|
||||
a.0 += (b - l) / 1.8556;
|
||||
a.1 += (r - l) / 1.5748;
|
||||
a.2 += 1.0;
|
||||
}
|
||||
}
|
||||
for (cx, a) in acc.iter().enumerate() {
|
||||
ur[cx] = round_pos(128.0 + 224.0 * a.0 / a.2).clamp(1.0, 254.0) as u8;
|
||||
vr[cx] = round_pos(128.0 + 224.0 * a.1 / a.2).clamp(1.0, 254.0) as u8;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The H.264 export's planes of `img` over `bg` (opaque): Y, then U and V, as
|
||||
/// [`crate::Image::to_rgba8_over`] + [`rgba_to_yuv420`] produce them.
|
||||
pub fn yuv420_over(img: &crate::Image, bg: [f32; 3]) -> Vec<u8> {
|
||||
let (mut y, mut u, mut v) = (vec![], vec![], vec![]);
|
||||
rgba_to_yuv420(&img.to_rgba8_over(bg), img.width as usize, img.height as usize, &mut y, &mut u, &mut v);
|
||||
y.extend_from_slice(&u);
|
||||
y.extend_from_slice(&v);
|
||||
y
|
||||
}
|
||||
Reference in New Issue
Block a user